@farcaster/snap 1.15.3 → 1.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/constants.d.ts +8 -0
  2. package/dist/constants.js +9 -1
  3. package/dist/index.d.ts +1 -1
  4. package/dist/index.js +1 -1
  5. package/dist/react/components/action-button.d.ts +2 -1
  6. package/dist/react/components/action-button.js +16 -3
  7. package/dist/react/components/badge.js +2 -3
  8. package/dist/react/index.d.ts +8 -1
  9. package/dist/react/index.js +9 -228
  10. package/dist/react/snap-view-core.d.ts +11 -0
  11. package/dist/react/snap-view-core.js +224 -0
  12. package/dist/react/v1/snap-view.d.ts +14 -0
  13. package/dist/react/v1/snap-view.js +9 -0
  14. package/dist/react/v2/snap-view.d.ts +21 -0
  15. package/dist/react/v2/snap-view.js +76 -0
  16. package/dist/react-native/components/snap-action-button.d.ts +1 -1
  17. package/dist/react-native/components/snap-action-button.js +19 -2
  18. package/dist/react-native/components/snap-badge.js +3 -3
  19. package/dist/react-native/index.d.ts +15 -43
  20. package/dist/react-native/index.js +10 -164
  21. package/dist/react-native/snap-view-core.d.ts +11 -0
  22. package/dist/react-native/snap-view-core.js +153 -0
  23. package/dist/react-native/types.d.ts +41 -0
  24. package/dist/react-native/types.js +1 -0
  25. package/dist/react-native/v1/snap-view.d.ts +22 -0
  26. package/dist/react-native/v1/snap-view.js +31 -0
  27. package/dist/react-native/v2/snap-view.d.ts +31 -0
  28. package/dist/react-native/v2/snap-view.js +101 -0
  29. package/dist/schemas.d.ts +15 -9
  30. package/dist/schemas.js +7 -8
  31. package/dist/server/parseRequest.d.ts +7 -0
  32. package/dist/server/parseRequest.js +27 -0
  33. package/dist/ui/catalog.d.ts +1 -0
  34. package/dist/ui/catalog.js +5 -2
  35. package/dist/ui/schema.js +1 -1
  36. package/dist/validator.d.ts +3 -2
  37. package/dist/validator.js +193 -2
  38. package/llms.txt +9 -0
  39. package/package.json +1 -1
  40. package/src/constants.ts +11 -1
  41. package/src/index.ts +8 -0
  42. package/src/react/accent-context.tsx +1 -1
  43. package/src/react/components/action-button.tsx +25 -3
  44. package/src/react/components/badge.tsx +2 -3
  45. package/src/react/index.tsx +36 -327
  46. package/src/react/snap-view-core.tsx +340 -0
  47. package/src/react/v1/snap-view.tsx +50 -0
  48. package/src/react/v2/snap-view.tsx +168 -0
  49. package/src/react-native/components/snap-action-button.tsx +26 -4
  50. package/src/react-native/components/snap-badge.tsx +3 -3
  51. package/src/react-native/index.tsx +47 -263
  52. package/src/react-native/snap-view-core.tsx +209 -0
  53. package/src/react-native/types.ts +37 -0
  54. package/src/react-native/v1/snap-view.tsx +108 -0
  55. package/src/react-native/v2/snap-view.tsx +239 -0
  56. package/src/schemas.ts +9 -10
  57. package/src/server/parseRequest.ts +39 -0
  58. package/src/ui/catalog.ts +5 -2
  59. package/src/ui/schema.ts +1 -1
  60. package/src/validator.ts +240 -2
@@ -0,0 +1,21 @@
1
+ import { type ReactNode } from "react";
2
+ import type { ValidationResult } from "../../validator.js";
3
+ import type { SnapPage, SnapActionHandlers } from "../index.js";
4
+ export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, }: {
5
+ snap: SnapPage;
6
+ handlers: SnapActionHandlers;
7
+ loading?: boolean;
8
+ appearance?: "light" | "dark";
9
+ onValidationError?: (result: ValidationResult) => void;
10
+ validationErrorFallback?: ReactNode;
11
+ }): import("react/jsx-runtime").JSX.Element | null;
12
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, }: {
13
+ snap: SnapPage;
14
+ handlers: SnapActionHandlers;
15
+ loading?: boolean;
16
+ appearance?: "light" | "dark";
17
+ maxWidth?: number;
18
+ showOverflowWarning?: boolean;
19
+ onValidationError?: (result: ValidationResult) => void;
20
+ validationErrorFallback?: ReactNode;
21
+ }): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,76 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useMemo } from "react";
4
+ import { validateSnapResponse } from "../../validator.js";
5
+ import { SnapViewCore } from "../snap-view-core.js";
6
+ const SNAP_MAX_HEIGHT = 500;
7
+ const SNAP_WARNING_HEIGHT = 700;
8
+ // ─── Default validation error fallback ────────────────
9
+ function SnapValidationFallback({ appearance, message, }) {
10
+ const isDark = appearance === "dark";
11
+ return (_jsx("div", { style: {
12
+ width: "100%",
13
+ padding: 16,
14
+ display: "flex",
15
+ alignItems: "center",
16
+ justifyContent: "center",
17
+ color: isDark ? "rgba(255,255,255,0.5)" : "rgba(0,0,0,0.4)",
18
+ fontSize: 14,
19
+ }, children: _jsx("span", { children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
20
+ }
21
+ // ─── SnapViewV2 ──────────────────────────────────────
22
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, }) {
23
+ const validation = useMemo(() => validateSnapResponse(snap), [snap]);
24
+ const valid = validation.valid;
25
+ const validationMessage = validation.issues[0]?.message;
26
+ useEffect(() => {
27
+ if (!valid) {
28
+ if (onValidationError) {
29
+ onValidationError(validation);
30
+ }
31
+ else {
32
+ // eslint-disable-next-line no-console
33
+ console.warn("[Snap] validation issues:", validation.issues);
34
+ }
35
+ }
36
+ }, [valid, validation, onValidationError]);
37
+ if (!valid) {
38
+ if (validationErrorFallback === null)
39
+ return null;
40
+ return _jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { appearance: appearance, message: validationMessage }) });
41
+ }
42
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance }));
43
+ }
44
+ // ─── SnapCardV2 ──────────────────────────────────────
45
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, }) {
46
+ const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
47
+ const bg = appearance === "dark" ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
48
+ return (_jsxs("div", { style: {
49
+ position: "relative",
50
+ width: "100%",
51
+ maxWidth,
52
+ maxHeight,
53
+ overflow: "hidden",
54
+ }, children: [_jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback }), showOverflowWarning && (_jsxs("div", { style: {
55
+ position: "absolute",
56
+ top: SNAP_MAX_HEIGHT,
57
+ left: 0,
58
+ right: 0,
59
+ bottom: 0,
60
+ pointerEvents: "none",
61
+ zIndex: 10,
62
+ }, children: [_jsx("div", { style: { borderTop: "1px dashed rgba(255,100,100,0.6)", position: "relative" }, children: _jsxs("span", { style: {
63
+ position: "absolute",
64
+ top: -10,
65
+ right: 0,
66
+ fontSize: 10,
67
+ fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace",
68
+ color: "rgba(255,100,100,0.7)",
69
+ background: bg,
70
+ padding: "1px 4px",
71
+ borderRadius: 3,
72
+ }, children: [SNAP_MAX_HEIGHT, "px"] }) }), _jsx("div", { style: {
73
+ height: "100%",
74
+ background: "repeating-linear-gradient(-45deg, transparent, transparent 8px, rgba(255,100,100,0.06) 8px, rgba(255,100,100,0.06) 16px)",
75
+ } })] }))] }));
76
+ }
@@ -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 },
@@ -15,11 +15,11 @@ export function SnapBadge({ element: { props }, }) {
15
15
  return (_jsxs(View, { style: [
16
16
  styles.badge,
17
17
  isFilled
18
- ? { backgroundColor: resolvedColor, borderColor: resolvedColor }
18
+ ? { backgroundColor: resolvedColor + "20", borderColor: "transparent" }
19
19
  : { borderColor: resolvedColor },
20
- ], children: [Icon && (_jsx(Icon, { size: 12, color: isFilled ? "#fff" : resolvedColor })), _jsx(Text, { style: [
20
+ ], children: [Icon && (_jsx(Icon, { size: 12, color: resolvedColor })), _jsx(Text, { style: [
21
21
  styles.label,
22
- { color: isFilled ? "#fff" : resolvedColor },
22
+ { color: resolvedColor },
23
23
  ], children: label })] }));
24
24
  }
25
25
  const styles = StyleSheet.create({
@@ -1,52 +1,24 @@
1
- import type { Spec } from "@json-render/core";
2
- import { useSnapTheme, type SnapNativeColors } from "./theme.js";
1
+ import type { ReactNode } from "react";
2
+ import type { ValidationResult } from "@farcaster/snap";
3
+ import type { SnapNativeColors } from "./theme.js";
4
+ import type { SnapPage, SnapActionHandlers } from "./types.js";
5
+ import { useSnapTheme } from "./theme.js";
3
6
  import { hexToRgba } from "./use-snap-palette.js";
4
- export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
- [key: string]: JsonValue;
6
- };
7
- export type SnapPage = {
8
- version: string;
9
- theme?: {
10
- accent?: string;
11
- };
12
- effects?: string[];
13
- ui: Spec;
14
- };
15
- export type SnapActionHandlers = {
16
- submit: (target: string, inputs: Record<string, JsonValue>) => void;
17
- open_url: (target: string) => void;
18
- open_mini_app: (target: string) => void;
19
- view_cast: (params: {
20
- hash: string;
21
- }) => void;
22
- view_profile: (params: {
23
- fid: number;
24
- }) => void;
25
- compose_cast: (params: {
26
- text?: string;
27
- channelKey?: string;
28
- embeds?: string[];
29
- }) => void;
30
- view_token: (params: {
31
- token: string;
32
- }) => void;
33
- send_token: (params: {
34
- token: string;
35
- amount?: string;
36
- recipientFid?: number;
37
- recipientAddress?: string;
38
- }) => void;
39
- swap_token: (params: {
40
- sellToken?: string;
41
- buyToken?: string;
42
- }) => void;
43
- };
7
+ export type { JsonValue, SnapPage, SnapActionHandlers } from "./types.js";
44
8
  export { useSnapTheme, hexToRgba };
45
9
  export type { SnapNativeColors };
46
- export declare function SnapView({ snap, handlers, loading, appearance, colors, }: {
10
+ export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, }: {
47
11
  snap: SnapPage;
48
12
  handlers: SnapActionHandlers;
49
13
  loading?: boolean;
50
14
  appearance?: "light" | "dark";
51
15
  colors?: Partial<SnapNativeColors>;
16
+ /** Border radius of the card (default 16). */
17
+ borderRadius?: number;
18
+ /** When true (v2 only), extends to 700px and shows a warning overlay below 500px. When false, clips at 500px. */
19
+ showOverflowWarning?: boolean;
20
+ /** Called when snap validation fails (v2 only). */
21
+ onValidationError?: (result: ValidationResult) => void;
22
+ /** Custom fallback rendered when validation fails (v2 only). */
23
+ validationErrorFallback?: ReactNode;
52
24
  }): import("react").JSX.Element;
@@ -1,169 +1,15 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
- import { SnapCatalogView } from "./catalog-renderer.js";
4
- import { SnapThemeProvider, useSnapTheme, } from "./theme.js";
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { SPEC_VERSION_2 } from "@farcaster/snap";
3
+ import { useSnapTheme } from "./theme.js";
5
4
  import { hexToRgba } from "./use-snap-palette.js";
6
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
- import { ActivityIndicator, StyleSheet, View } from "react-native";
8
- import { ConfettiOverlay } from "./confetti-overlay.js";
9
- import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
5
+ import { SnapCardV1 } from "./v1/snap-view.js";
6
+ import { SnapCardV2 } from "./v2/snap-view.js";
10
7
  // ─── Re-exports ───────────────────────────────────────
11
8
  export { useSnapTheme, hexToRgba };
12
- // ─── Internal helpers ─────────────────────────────────
13
- function applyStatePaths(model, changes) {
14
- const entries = Array.isArray(changes)
15
- ? changes.map((c) => [c.path, c.value])
16
- : Object.entries(changes);
17
- for (const [path, value] of entries) {
18
- const trimmed = path.startsWith("/") ? path : `/${path}`;
19
- const parts = trimmed.split("/").filter(Boolean);
20
- if (parts.length < 2)
21
- continue;
22
- const [top, ...rest] = parts;
23
- if (top === "inputs") {
24
- if (typeof model.inputs !== "object" || model.inputs === null) {
25
- model.inputs = {};
26
- }
27
- const inputs = model.inputs;
28
- if (rest.length === 1) {
29
- inputs[rest[0]] = value;
30
- }
31
- continue;
32
- }
33
- if (top === "theme") {
34
- if (typeof model.theme !== "object" || model.theme === null) {
35
- model.theme = {};
36
- }
37
- const theme = model.theme;
38
- if (rest.length === 1) {
39
- theme[rest[0]] = value;
40
- }
41
- }
9
+ // ─── SnapCard (version-switching) ─────────────────────
10
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, }) {
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 }));
42
13
  }
14
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius }));
43
15
  }
44
- function resolveAccentHex(accent, appearance) {
45
- const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
46
- const name = accent && Object.hasOwn(map, accent)
47
- ? accent
48
- : DEFAULT_THEME_ACCENT;
49
- return map[name];
50
- }
51
- // ─── SnapView ─────────────────────────────────────────
52
- function SnapViewInner({ snap, handlers, loading = false, }) {
53
- const { mode } = useSnapTheme();
54
- const spec = snap.ui;
55
- const accentHex = resolveAccentHex(snap.theme?.accent, mode);
56
- const showConfetti = snap.effects?.includes("confetti");
57
- // Increment key each time a new snap with confetti arrives so the overlay
58
- // unmounts/remounts and restarts its animation on every trigger.
59
- const confettiEpochRef = useRef(0);
60
- const lastConfettiSnapRef = useRef(null);
61
- if (showConfetti && snap !== lastConfettiSnapRef.current) {
62
- confettiEpochRef.current++;
63
- lastConfettiSnapRef.current = snap;
64
- }
65
- const initialState = useMemo(() => ({
66
- ...(spec.state ?? {}),
67
- inputs: { ...(spec.state?.inputs ?? {}) },
68
- theme: {
69
- ...(spec.state?.theme ?? {}),
70
- ...(snap.theme ? { accent: snap.theme.accent } : {}),
71
- },
72
- }), [spec, snap.theme]);
73
- const stateRef = useRef(initialState);
74
- useEffect(() => {
75
- stateRef.current = {
76
- inputs: {
77
- ...(initialState.inputs ?? {}),
78
- },
79
- theme: {
80
- ...(initialState.theme ?? {}),
81
- },
82
- };
83
- }, [initialState]);
84
- useEffect(() => {
85
- const result = snapJsonRenderCatalog.validate(spec);
86
- if (!result.success) {
87
- // eslint-disable-next-line no-console
88
- console.warn("[SnapView] catalog validation issues:", result.error);
89
- }
90
- }, [spec]);
91
- const [pageKey, setPageKey] = useState(0);
92
- useEffect(() => {
93
- setPageKey((k) => k + 1);
94
- }, [spec]);
95
- const handlersRef = useRef(handlers);
96
- handlersRef.current = handlers;
97
- const handleAction = useCallback((name, params) => {
98
- const inputs = (stateRef.current.inputs ?? {});
99
- const p = (params ?? {});
100
- const h = handlersRef.current;
101
- switch (name) {
102
- case "submit":
103
- h.submit(String(p.target ?? ""), inputs);
104
- break;
105
- case "open_url":
106
- h.open_url(String(p.target ?? ""));
107
- break;
108
- case "open_mini_app":
109
- h.open_mini_app(String(p.target ?? ""));
110
- break;
111
- case "view_cast":
112
- h.view_cast({ hash: String(p.hash ?? "") });
113
- break;
114
- case "view_profile":
115
- h.view_profile({ fid: Number(p.fid ?? 0) });
116
- break;
117
- case "compose_cast":
118
- h.compose_cast({
119
- text: p.text ? String(p.text) : undefined,
120
- channelKey: p.channelKey ? String(p.channelKey) : undefined,
121
- embeds: Array.isArray(p.embeds) ? p.embeds : undefined,
122
- });
123
- break;
124
- case "view_token":
125
- h.view_token({ token: String(p.token ?? "") });
126
- break;
127
- case "send_token":
128
- h.send_token({
129
- token: String(p.token ?? ""),
130
- amount: p.amount ? String(p.amount) : undefined,
131
- recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
132
- recipientAddress: p.recipientAddress
133
- ? String(p.recipientAddress)
134
- : undefined,
135
- });
136
- break;
137
- case "swap_token":
138
- h.swap_token({
139
- sellToken: p.sellToken ? String(p.sellToken) : undefined,
140
- buyToken: p.buyToken ? String(p.buyToken) : undefined,
141
- });
142
- break;
143
- default:
144
- break;
145
- }
146
- }, []);
147
- return (_jsxs(View, { style: styles.container, children: [loading ? (_jsx(View, { style: [
148
- styles.overlay,
149
- {
150
- backgroundColor: mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
151
- },
152
- ], 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
- applyStatePaths(stateRef.current, changes);
154
- }, onAction: handleAction }, pageKey)] }));
155
- }
156
- export function SnapView({ snap, handlers, loading = false, appearance = "dark", colors, }) {
157
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewInner, { snap: snap, handlers: handlers, loading: loading }) }));
158
- }
159
- const styles = StyleSheet.create({
160
- container: {
161
- width: "100%",
162
- },
163
- overlay: {
164
- ...StyleSheet.absoluteFillObject,
165
- alignItems: "center",
166
- justifyContent: "center",
167
- zIndex: 10,
168
- },
169
- });
@@ -0,0 +1,11 @@
1
+ import type { SnapPage, SnapActionHandlers } from "./types.js";
2
+ export declare function applyStatePaths(model: Record<string, unknown>, changes: {
3
+ path: string;
4
+ value: unknown;
5
+ }[] | Record<string, unknown>): void;
6
+ export declare function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string;
7
+ export declare function SnapViewCoreInner({ snap, handlers, loading, }: {
8
+ snap: SnapPage;
9
+ handlers: SnapActionHandlers;
10
+ loading?: boolean;
11
+ }): import("react").JSX.Element;
@@ -0,0 +1,153 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapCatalogView } from "./catalog-renderer.js";
4
+ import { useSnapTheme } from "./theme.js";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
7
+ import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
8
+ // ─── Shared helpers ──────────────────────────────────
9
+ export function applyStatePaths(model, changes) {
10
+ const entries = Array.isArray(changes)
11
+ ? changes.map((c) => [c.path, c.value])
12
+ : Object.entries(changes);
13
+ for (const [path, value] of entries) {
14
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
15
+ const parts = trimmed.split("/").filter(Boolean);
16
+ if (parts.length < 2)
17
+ continue;
18
+ const [top, ...rest] = parts;
19
+ if (top === "inputs") {
20
+ if (typeof model.inputs !== "object" || model.inputs === null) {
21
+ model.inputs = {};
22
+ }
23
+ const inputs = model.inputs;
24
+ if (rest.length === 1) {
25
+ inputs[rest[0]] = value;
26
+ }
27
+ continue;
28
+ }
29
+ if (top === "theme") {
30
+ if (typeof model.theme !== "object" || model.theme === null) {
31
+ model.theme = {};
32
+ }
33
+ const theme = model.theme;
34
+ if (rest.length === 1) {
35
+ theme[rest[0]] = value;
36
+ }
37
+ }
38
+ }
39
+ }
40
+ export function resolveAccentHex(accent, appearance) {
41
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
42
+ const name = accent && Object.hasOwn(map, accent)
43
+ ? accent
44
+ : DEFAULT_THEME_ACCENT;
45
+ return map[name];
46
+ }
47
+ // ─── Core rendering component (no validation) ────────
48
+ export function SnapViewCoreInner({ snap, handlers, loading = false, }) {
49
+ const { mode } = useSnapTheme();
50
+ const spec = snap.ui;
51
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
52
+ const initialState = useMemo(() => ({
53
+ ...(spec.state ?? {}),
54
+ inputs: { ...(spec.state?.inputs ?? {}) },
55
+ theme: {
56
+ ...(spec.state?.theme ?? {}),
57
+ ...(snap.theme ? { accent: snap.theme.accent } : {}),
58
+ },
59
+ }), [spec, snap.theme]);
60
+ const stateRef = useRef(initialState);
61
+ useEffect(() => {
62
+ stateRef.current = {
63
+ inputs: {
64
+ ...(initialState.inputs ?? {}),
65
+ },
66
+ theme: {
67
+ ...(initialState.theme ?? {}),
68
+ },
69
+ };
70
+ }, [initialState]);
71
+ useEffect(() => {
72
+ const catalogResult = snapJsonRenderCatalog.validate(spec);
73
+ if (!catalogResult.success) {
74
+ // eslint-disable-next-line no-console
75
+ console.warn("[Snap] catalog validation issues:", catalogResult.error);
76
+ }
77
+ }, [spec]);
78
+ const [pageKey, setPageKey] = useState(0);
79
+ useEffect(() => {
80
+ setPageKey((k) => k + 1);
81
+ }, [spec]);
82
+ const handlersRef = useRef(handlers);
83
+ handlersRef.current = handlers;
84
+ const handleAction = useCallback((name, params) => {
85
+ const inputs = (stateRef.current.inputs ?? {});
86
+ const p = (params ?? {});
87
+ const h = handlersRef.current;
88
+ switch (name) {
89
+ case "submit":
90
+ h.submit(String(p.target ?? ""), inputs);
91
+ break;
92
+ case "open_url":
93
+ h.open_url(String(p.target ?? ""));
94
+ break;
95
+ case "open_mini_app":
96
+ h.open_mini_app(String(p.target ?? ""));
97
+ break;
98
+ case "view_cast":
99
+ h.view_cast({ hash: String(p.hash ?? "") });
100
+ break;
101
+ case "view_profile":
102
+ h.view_profile({ fid: Number(p.fid ?? 0) });
103
+ break;
104
+ case "compose_cast":
105
+ h.compose_cast({
106
+ text: p.text ? String(p.text) : undefined,
107
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
108
+ embeds: Array.isArray(p.embeds) ? p.embeds : undefined,
109
+ });
110
+ break;
111
+ case "view_token":
112
+ h.view_token({ token: String(p.token ?? "") });
113
+ break;
114
+ case "send_token":
115
+ h.send_token({
116
+ token: String(p.token ?? ""),
117
+ amount: p.amount ? String(p.amount) : undefined,
118
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
119
+ recipientAddress: p.recipientAddress
120
+ ? String(p.recipientAddress)
121
+ : undefined,
122
+ });
123
+ break;
124
+ case "swap_token":
125
+ h.swap_token({
126
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
127
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
128
+ });
129
+ break;
130
+ default:
131
+ break;
132
+ }
133
+ }, []);
134
+ return (_jsxs(View, { style: styles.container, children: [loading ? (_jsx(View, { style: [
135
+ styles.overlay,
136
+ {
137
+ backgroundColor: mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
138
+ },
139
+ ], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) })) : null, _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
140
+ applyStatePaths(stateRef.current, changes);
141
+ }, onAction: handleAction }, pageKey)] }));
142
+ }
143
+ const styles = StyleSheet.create({
144
+ container: {
145
+ width: "100%",
146
+ },
147
+ overlay: {
148
+ ...StyleSheet.absoluteFillObject,
149
+ alignItems: "center",
150
+ justifyContent: "center",
151
+ zIndex: 10,
152
+ },
153
+ });
@@ -0,0 +1,41 @@
1
+ import type { Spec } from "@json-render/core";
2
+ export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
+ [key: string]: JsonValue;
4
+ };
5
+ export type SnapPage = {
6
+ version: string;
7
+ theme?: {
8
+ accent?: string;
9
+ };
10
+ effects?: string[];
11
+ ui: Spec;
12
+ };
13
+ export type SnapActionHandlers = {
14
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
+ open_url: (target: string) => void;
16
+ open_mini_app: (target: string) => void;
17
+ view_cast: (params: {
18
+ hash: string;
19
+ }) => void;
20
+ view_profile: (params: {
21
+ fid: number;
22
+ }) => void;
23
+ compose_cast: (params: {
24
+ text?: string;
25
+ channelKey?: string;
26
+ embeds?: string[];
27
+ }) => void;
28
+ view_token: (params: {
29
+ token: string;
30
+ }) => void;
31
+ send_token: (params: {
32
+ token: string;
33
+ amount?: string;
34
+ recipientFid?: number;
35
+ recipientAddress?: string;
36
+ }) => void;
37
+ swap_token: (params: {
38
+ sellToken?: string;
39
+ buyToken?: string;
40
+ }) => void;
41
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,22 @@
1
+ import { type SnapNativeColors } from "../theme.js";
2
+ import type { SnapPage, SnapActionHandlers } from "../types.js";
3
+ export declare function SnapViewV1Inner({ snap, handlers, loading, }: {
4
+ snap: SnapPage;
5
+ handlers: SnapActionHandlers;
6
+ loading?: boolean;
7
+ }): import("react").JSX.Element;
8
+ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, }: {
9
+ snap: SnapPage;
10
+ handlers: SnapActionHandlers;
11
+ loading?: boolean;
12
+ appearance?: "light" | "dark";
13
+ colors?: Partial<SnapNativeColors>;
14
+ }): import("react").JSX.Element;
15
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, }: {
16
+ snap: SnapPage;
17
+ handlers: SnapActionHandlers;
18
+ loading?: boolean;
19
+ appearance?: "light" | "dark";
20
+ colors?: Partial<SnapNativeColors>;
21
+ borderRadius?: number;
22
+ }): import("react").JSX.Element;