@farcaster/snap 2.6.4 → 2.7.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.
@@ -8,39 +8,14 @@ import { SnapVersionProvider } from "./snap-version-context.js";
8
8
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
9
9
  import { ActivityIndicator, StyleSheet, View } from "react-native";
10
10
  import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
11
- // ─── Shared helpers ──────────────────────────────────
12
- export function applyStatePaths(model, changes) {
13
- if (!changes)
14
- return;
15
- const entries = Array.isArray(changes)
16
- ? changes.map((c) => [c.path, c.value])
17
- : Object.entries(changes);
18
- for (const [path, value] of entries) {
19
- const trimmed = path.startsWith("/") ? path : `/${path}`;
20
- const parts = trimmed.split("/").filter(Boolean);
21
- if (parts.length < 2)
22
- continue;
23
- const [top, ...rest] = parts;
24
- if (top === "inputs") {
25
- if (typeof model.inputs !== "object" || model.inputs === null) {
26
- model.inputs = {};
27
- }
28
- const inputs = model.inputs;
29
- if (rest.length === 1) {
30
- inputs[rest[0]] = value;
31
- }
32
- continue;
33
- }
34
- if (top === "theme") {
35
- if (typeof model.theme !== "object" || model.theme === null) {
36
- model.theme = {};
37
- }
38
- const theme = model.theme;
39
- if (rest.length === 1) {
40
- theme[rest[0]] = value;
41
- }
42
- }
43
- }
11
+ import { applyStatePaths, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, markSnapEffectsPresented, } from "../render-state.js";
12
+ function asRecord(value) {
13
+ return value && typeof value === "object"
14
+ ? value
15
+ : {};
16
+ }
17
+ function optionalString(value) {
18
+ return value ? String(value) : undefined;
44
19
  }
45
20
  function withDefaultElementProps(spec) {
46
21
  if (!spec || typeof spec !== "object" || !("elements" in spec))
@@ -70,28 +45,18 @@ export function resolveAccentHex(accent, appearance) {
70
45
  return map[name];
71
46
  }
72
47
  // ─── Core rendering component (no validation) ────────
73
- export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOverlay, }) {
48
+ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOverlay, initialRenderState, onRenderStateChange, }) {
74
49
  const { mode } = useSnapTheme();
75
50
  const spec = useMemo(() => withDefaultElementProps(snap.ui), [snap.ui]);
76
51
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
77
- const initialState = useMemo(() => ({
78
- ...(spec.state ?? {}),
79
- inputs: { ...(spec.state?.inputs ?? {}) },
80
- theme: {
81
- ...(spec.state?.theme ?? {}),
82
- ...(snap.theme ? { accent: snap.theme.accent } : {}),
83
- },
84
- }), [spec, snap.theme]);
52
+ const initialState = useMemo(() => buildInitialRenderState({
53
+ specState: spec.state,
54
+ initialRenderState,
55
+ themeAccent: snap.theme?.accent,
56
+ }), [initialRenderState, spec.state, snap.theme?.accent]);
85
57
  const stateRef = useRef(initialState);
86
58
  useEffect(() => {
87
- stateRef.current = {
88
- inputs: {
89
- ...(initialState.inputs ?? {}),
90
- },
91
- theme: {
92
- ...(initialState.theme ?? {}),
93
- },
94
- };
59
+ stateRef.current = cloneSnapRenderState(initialState);
95
60
  }, [initialState]);
96
61
  useEffect(() => {
97
62
  const catalogResult = snapJsonRenderCatalog.validate(spec);
@@ -104,16 +69,49 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
104
69
  useEffect(() => {
105
70
  setPageKey((k) => k + 1);
106
71
  }, [spec]);
107
- const showConfetti = snap.effects?.includes("confetti") ?? false;
108
- const showFireworks = snap.effects?.includes("fireworks") ?? false;
109
- const [confettiKey, setConfettiKey] = useState(0);
110
- const [fireworksKey, setFireworksKey] = useState(0);
72
+ const effectSignature = snap.effects?.join("\u0000") ?? "";
73
+ const snapEffects = useMemo(() => (effectSignature ? effectSignature.split("\u0000") : []), [effectSignature]);
74
+ const showConfetti = snapEffects.includes("confetti");
75
+ const showFireworks = snapEffects.includes("fireworks");
76
+ const [effectRunKeys, setEffectRunKeys] = useState({
77
+ confetti: 0,
78
+ fireworks: 0,
79
+ });
80
+ const onRenderStateChangeRef = useRef(onRenderStateChange);
111
81
  useEffect(() => {
112
- if (showConfetti)
113
- setConfettiKey((k) => k + 1);
114
- if (showFireworks)
115
- setFireworksKey((k) => k + 1);
116
- }, [showConfetti, showFireworks, snap]);
82
+ onRenderStateChangeRef.current = onRenderStateChange;
83
+ }, [onRenderStateChange]);
84
+ useEffect(() => {
85
+ const effectsToPresent = getUnpresentedSnapEffects(stateRef.current, snapEffects);
86
+ if (effectsToPresent.length === 0) {
87
+ setEffectRunKeys((current) => {
88
+ const next = {
89
+ confetti: showConfetti ? current.confetti : 0,
90
+ fireworks: showFireworks ? current.fireworks : 0,
91
+ };
92
+ return next.confetti === current.confetti &&
93
+ next.fireworks === current.fireworks
94
+ ? current
95
+ : next;
96
+ });
97
+ return;
98
+ }
99
+ if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
100
+ onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
101
+ }
102
+ setEffectRunKeys((current) => ({
103
+ confetti: effectsToPresent.includes("confetti")
104
+ ? current.confetti + 1
105
+ : showConfetti
106
+ ? current.confetti
107
+ : 0,
108
+ fireworks: effectsToPresent.includes("fireworks")
109
+ ? current.fireworks + 1
110
+ : showFireworks
111
+ ? current.fireworks
112
+ : 0,
113
+ }));
114
+ }, [initialState, showConfetti, showFireworks, snapEffects]);
117
115
  const handlersRef = useRef(handlers);
118
116
  handlersRef.current = handlers;
119
117
  const handleAction = useCallback((name, params) => {
@@ -165,25 +163,52 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
165
163
  buyToken: p.buyToken ? String(p.buyToken) : undefined,
166
164
  });
167
165
  break;
166
+ case "send_transaction":
167
+ h.send_transaction?.({
168
+ chainId: String(p.chainId ?? ""),
169
+ to: String(p.to ?? ""),
170
+ data: optionalString(p.data),
171
+ value: optionalString(p.value),
172
+ gas: optionalString(p.gas),
173
+ gasPrice: optionalString(p.gasPrice),
174
+ maxFeePerGas: optionalString(p.maxFeePerGas),
175
+ maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
176
+ });
177
+ break;
178
+ case "send_calls":
179
+ h.send_calls?.({
180
+ version: p.version === "1.0" ? "1.0" : undefined,
181
+ chainId: String(p.chainId ?? ""),
182
+ atomicRequired: typeof p.atomicRequired === "boolean"
183
+ ? p.atomicRequired
184
+ : undefined,
185
+ id: optionalString(p.id),
186
+ calls: Array.isArray(p.calls)
187
+ ? p.calls.map((call) => {
188
+ const c = asRecord(call);
189
+ return {
190
+ to: optionalString(c.to),
191
+ data: optionalString(c.data),
192
+ value: optionalString(c.value),
193
+ };
194
+ })
195
+ : [],
196
+ });
197
+ break;
168
198
  default:
169
199
  break;
170
200
  }
171
201
  }, []);
172
- return (_jsxs(View, { style: styles.container, children: [loading
173
- ? loadingOverlay === undefined
174
- ? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex }))
175
- : loadingOverlay
176
- : null, _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
202
+ return (_jsxs(View, { style: styles.container, children: [loading ? (loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })) : (loadingOverlay)) : null, _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
177
203
  applyStatePaths(stateRef.current, changes);
178
- }, onAction: handleAction }, pageKey) }), showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey)] }));
204
+ onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
205
+ }, onAction: handleAction }, pageKey) }), showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks))] }));
179
206
  }
180
207
  export function SnapLoadingOverlay({ appearance, accentHex, }) {
181
208
  return (_jsx(View, { style: [
182
209
  styles.overlay,
183
210
  {
184
- backgroundColor: appearance === "dark"
185
- ? "rgba(0,0,0,0.1)"
186
- : "rgba(255,255,255,0.2)",
211
+ backgroundColor: appearance === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
187
212
  },
188
213
  ], children: _jsx(ActivityIndicator, { size: "large", color: accentHex }) }));
189
214
  }
@@ -1,4 +1,6 @@
1
1
  import type { Spec } from "@json-render/core";
2
+ import type { SnapRenderState } from "../render-state.js";
3
+ export type { SnapRenderState };
2
4
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
3
5
  [key: string]: JsonValue;
4
6
  };
@@ -10,6 +12,27 @@ export type SnapPage = {
10
12
  effects?: string[];
11
13
  ui: Spec;
12
14
  };
15
+ export type SnapSendTransactionParams = {
16
+ chainId: string;
17
+ to: string;
18
+ data?: string;
19
+ value?: string;
20
+ gas?: string;
21
+ gasPrice?: string;
22
+ maxFeePerGas?: string;
23
+ maxPriorityFeePerGas?: string;
24
+ };
25
+ export type SnapSendCallsParams = {
26
+ version?: "1.0";
27
+ chainId: string;
28
+ atomicRequired?: boolean;
29
+ id?: string;
30
+ calls: Array<{
31
+ to?: string;
32
+ data?: string;
33
+ value?: string;
34
+ }>;
35
+ };
13
36
  export type SnapActionHandlers = {
14
37
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
15
38
  open_url: (target: string) => void;
@@ -39,4 +62,6 @@ export type SnapActionHandlers = {
39
62
  sellToken?: string;
40
63
  buyToken?: string;
41
64
  }) => void;
65
+ send_transaction?: (params: SnapSendTransactionParams) => void;
66
+ send_calls?: (params: SnapSendCallsParams) => void;
42
67
  };
@@ -1,13 +1,15 @@
1
1
  import { type ReactNode } from "react";
2
2
  import { type SnapNativeColors } from "../theme.js";
3
- import type { SnapPage, SnapActionHandlers } from "../types.js";
4
- export declare function SnapViewV1Inner({ snap, handlers, loading, loadingOverlay, }: {
3
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../types.js";
4
+ export declare function SnapViewV1Inner({ snap, handlers, loading, loadingOverlay, initialRenderState, onRenderStateChange, }: {
5
5
  snap: SnapPage;
6
6
  handlers: SnapActionHandlers;
7
7
  loading?: boolean;
8
8
  loadingOverlay?: ReactNode;
9
+ initialRenderState?: SnapRenderState;
10
+ onRenderStateChange?: (state: SnapRenderState) => void;
9
11
  }): import("react").JSX.Element;
10
- export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, loadingOverlay, }: {
12
+ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors, loadingOverlay, initialRenderState, onRenderStateChange, }: {
11
13
  snap: SnapPage;
12
14
  handlers: SnapActionHandlers;
13
15
  loading?: boolean;
@@ -15,8 +17,10 @@ export declare function SnapViewV1({ snap, handlers, loading, appearance, colors
15
17
  colors?: Partial<SnapNativeColors>;
16
18
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
17
19
  loadingOverlay?: ReactNode;
20
+ initialRenderState?: SnapRenderState;
21
+ onRenderStateChange?: (state: SnapRenderState) => void;
18
22
  }): import("react").JSX.Element;
19
- export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
23
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors, borderRadius, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }: {
20
24
  snap: SnapPage;
21
25
  handlers: SnapActionHandlers;
22
26
  loading?: boolean;
@@ -33,4 +37,8 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, colors
33
37
  expandButtonLabel?: string;
34
38
  /** Called from the collapsed expand button instead of toggling internal state. */
35
39
  onExpandPress?: () => void;
40
+ /** JSON-render local state used to seed this presenter mount. */
41
+ initialRenderState?: SnapRenderState;
42
+ /** Called with the full JSON-render local state after state changes. */
43
+ onRenderStateChange?: (state: SnapRenderState) => void;
36
44
  }): import("react").JSX.Element;
@@ -5,14 +5,14 @@ import { SnapThemeProvider, useSnapTheme } from "../theme.js";
5
5
  import { SnapLoadingOverlay, SnapViewCoreInner, resolveAccentHex, } from "../snap-view-core.js";
6
6
  import { getSnapExpansionState } from "../expand-state.js";
7
7
  // ─── SnapViewV1 (no validation) ──────────────────────
8
- export function SnapViewV1Inner({ snap, handlers, loading = false, loadingOverlay, }) {
9
- return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
8
+ export function SnapViewV1Inner({ snap, handlers, loading = false, loadingOverlay, initialRenderState, onRenderStateChange, }) {
9
+ return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
10
10
  }
11
- export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, loadingOverlay, }) {
12
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }) }));
11
+ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", colors, loadingOverlay, initialRenderState, onRenderStateChange, }) {
12
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }));
13
13
  }
14
14
  // ─── SnapCardV1 (card frame with expandable clipping) ──
15
- function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
15
+ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, actionError, appearance, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }) {
16
16
  const { colors, mode } = useSnapTheme();
17
17
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
18
18
  const [contentHeight, setContentHeight] = useState(0);
@@ -48,7 +48,7 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
48
48
  : currentHeight === nextHeight
49
49
  ? currentHeight
50
50
  : nextHeight);
51
- }, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: null }) }) }), loading
51
+ }, style: plain ? undefined : cardStyles.body, children: _jsx(SnapViewV1Inner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: null, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }) }), loading
52
52
  ? loadingOverlay === undefined
53
53
  ? _jsx(SnapLoadingOverlay, { appearance: mode, accentHex: accentHex })
54
54
  : loadingOverlay
@@ -76,8 +76,8 @@ function SnapCardV1Inner({ snap, handlers, loading = false, borderRadius, action
76
76
  },
77
77
  ], children: actionError }))] }));
78
78
  }
79
- export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
80
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }) }));
79
+ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }) {
80
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV1Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }));
81
81
  }
82
82
  const cardStyles = StyleSheet.create({
83
83
  frameRing: { alignSelf: "stretch" },
@@ -1,16 +1,18 @@
1
1
  import type { ReactNode } from "react";
2
2
  import { type SnapNativeColors } from "../theme.js";
3
3
  import { type ValidationResult } from "@farcaster/snap";
4
- import type { SnapPage, SnapActionHandlers } from "../types.js";
5
- export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, loadingOverlay, }: {
4
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../types.js";
5
+ export declare function SnapViewV2Inner({ snap, handlers, loading, onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }: {
6
6
  snap: SnapPage;
7
7
  handlers: SnapActionHandlers;
8
8
  loading?: boolean;
9
9
  onValidationError?: (result: ValidationResult) => void;
10
10
  validationErrorFallback?: ReactNode;
11
11
  loadingOverlay?: ReactNode;
12
+ initialRenderState?: SnapRenderState;
13
+ onRenderStateChange?: (state: SnapRenderState) => void;
12
14
  }): import("react").JSX.Element;
13
- export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, loadingOverlay, }: {
15
+ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors, onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }: {
14
16
  snap: SnapPage;
15
17
  handlers: SnapActionHandlers;
16
18
  loading?: boolean;
@@ -20,8 +22,10 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, colors
20
22
  validationErrorFallback?: ReactNode;
21
23
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
22
24
  loadingOverlay?: ReactNode;
25
+ initialRenderState?: SnapRenderState;
26
+ onRenderStateChange?: (state: SnapRenderState) => void;
23
27
  }): import("react").JSX.Element;
24
- export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
28
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }: {
25
29
  snap: SnapPage;
26
30
  handlers: SnapActionHandlers;
27
31
  loading?: boolean;
@@ -41,4 +45,8 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, colors
41
45
  expandButtonLabel?: string;
42
46
  /** Called from the collapsed expand button instead of toggling internal state. */
43
47
  onExpandPress?: () => void;
48
+ /** JSON-render local state used to seed this presenter mount. */
49
+ initialRenderState?: SnapRenderState;
50
+ /** Called with the full JSON-render local state after state changes. */
51
+ onRenderStateChange?: (state: SnapRenderState) => void;
44
52
  }): import("react").JSX.Element;
@@ -25,7 +25,7 @@ const fallbackStyles = StyleSheet.create({
25
25
  },
26
26
  });
27
27
  // ─── SnapViewV2 (with validation) ────────────────────
28
- export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, loadingOverlay, }) {
28
+ export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }) {
29
29
  const validation = useMemo(() => validateSnapResponse(snap), [snap]);
30
30
  const valid = validation.valid;
31
31
  const validationMessage = validation.issues[0]?.message;
@@ -45,13 +45,13 @@ export function SnapViewV2Inner({ snap, handlers, loading = false, onValidationE
45
45
  return null;
46
46
  return (_jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { message: validationMessage }) }));
47
47
  }
48
- return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay }));
48
+ return (_jsx(SnapViewCoreInner, { snap: snap, handlers: handlers, loading: loading, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
49
49
  }
50
- export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, loadingOverlay, }) {
51
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: loadingOverlay }) }));
50
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", colors, onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }) {
51
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }));
52
52
  }
53
53
  // ─── SnapCardV2 (card frame + height limits) ─────────
54
- function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
54
+ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, appearance, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }) {
55
55
  const { colors, mode } = useSnapTheme();
56
56
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
57
57
  const [contentHeight, setContentHeight] = useState(0);
@@ -69,7 +69,7 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
69
69
  showOverflowWarning,
70
70
  });
71
71
  const expandButtonInsideCard = typeof onExpandPress === "function";
72
- const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }));
72
+ const content = (_jsx(SnapViewV2Inner, { snap: snap, handlers: handlers, loading: loading, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
73
73
  if (plain) {
74
74
  return (_jsxs(_Fragment, { children: [_jsx(View, { style: expansion.clipped
75
75
  ? { maxHeight: expansion.maxHeight, overflow: "hidden" }
@@ -151,8 +151,8 @@ function SnapCardV2Inner({ snap, handlers, loading, borderRadius, showOverflowWa
151
151
  : "rgba(200,0,0,0.8)",
152
152
  }, children: actionError }))] }));
153
153
  }
154
- export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
155
- return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }) }));
154
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }) {
155
+ return (_jsx(SnapThemeProvider, { appearance: appearance, colors: colors, children: _jsx(SnapCardV2Inner, { snap: snap, handlers: handlers, loading: loading, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, appearance: appearance, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }));
156
156
  }
157
157
  const cardStyles = StyleSheet.create({
158
158
  frameRing: { alignSelf: "stretch" },
@@ -0,0 +1,14 @@
1
+ export type SnapRenderState = Record<string, unknown>;
2
+ export type SnapRenderStateChanges = {
3
+ path: string;
4
+ value: unknown;
5
+ }[] | Record<string, unknown> | null | undefined;
6
+ export declare function cloneSnapRenderState<T>(value: T): T;
7
+ export declare function applyStatePaths(model: Record<string, unknown>, changes: SnapRenderStateChanges): void;
8
+ export declare function getUnpresentedSnapEffects(model: SnapRenderState, effects: readonly string[] | undefined): string[];
9
+ export declare function markSnapEffectsPresented(model: SnapRenderState, effects: readonly string[] | undefined): boolean;
10
+ export declare function buildInitialRenderState({ specState, initialRenderState, themeAccent, }: {
11
+ specState: unknown;
12
+ initialRenderState?: SnapRenderState;
13
+ themeAccent?: string;
14
+ }): SnapRenderState;
@@ -0,0 +1,116 @@
1
+ const SNAP_RENDER_STATE_META_KEY = "__snapRender";
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ function normalizeEffects(effects) {
6
+ if (!effects)
7
+ return [];
8
+ return Array.from(new Set(effects.filter((effect) => typeof effect === "string" && effect.length > 0)));
9
+ }
10
+ function getRenderStateMeta(model) {
11
+ const meta = model[SNAP_RENDER_STATE_META_KEY];
12
+ return isRecord(meta) ? meta : undefined;
13
+ }
14
+ function getPresentedSnapEffects(model) {
15
+ const presentedEffects = getRenderStateMeta(model)?.presentedEffects;
16
+ if (!Array.isArray(presentedEffects))
17
+ return new Set();
18
+ return new Set(presentedEffects.filter((effect) => typeof effect === "string" && effect.length > 0));
19
+ }
20
+ export function cloneSnapRenderState(value) {
21
+ if (Array.isArray(value)) {
22
+ return value.map((item) => cloneSnapRenderState(item));
23
+ }
24
+ if (isRecord(value)) {
25
+ const next = {};
26
+ for (const [key, nestedValue] of Object.entries(value)) {
27
+ next[key] = cloneSnapRenderState(nestedValue);
28
+ }
29
+ return next;
30
+ }
31
+ return value;
32
+ }
33
+ function mergeRenderState(base, override) {
34
+ if (!override)
35
+ return cloneSnapRenderState(base);
36
+ const next = cloneSnapRenderState(base);
37
+ for (const [key, value] of Object.entries(override)) {
38
+ const existing = next[key];
39
+ next[key] =
40
+ isRecord(existing) && isRecord(value)
41
+ ? mergeRenderState(existing, value)
42
+ : cloneSnapRenderState(value);
43
+ }
44
+ return next;
45
+ }
46
+ function normalizeStatePath(path) {
47
+ const trimmed = path.startsWith("/") ? path.slice(1) : path;
48
+ return trimmed.split("/").filter(Boolean);
49
+ }
50
+ function setStateValue(model, parts, value) {
51
+ let cursor = model;
52
+ for (let index = 0; index < parts.length; index += 1) {
53
+ const part = parts[index];
54
+ if (index === parts.length - 1) {
55
+ cursor[part] = cloneSnapRenderState(value);
56
+ return;
57
+ }
58
+ const next = cursor[part];
59
+ if (!isRecord(next)) {
60
+ cursor[part] = {};
61
+ }
62
+ cursor = cursor[part];
63
+ }
64
+ }
65
+ export function applyStatePaths(model, changes) {
66
+ if (!changes)
67
+ return;
68
+ const entries = Array.isArray(changes)
69
+ ? changes.map((change) => [change.path, change.value])
70
+ : Object.entries(changes);
71
+ for (const [path, value] of entries) {
72
+ const parts = normalizeStatePath(path);
73
+ if (parts.length === 0)
74
+ continue;
75
+ setStateValue(model, parts, value);
76
+ }
77
+ }
78
+ export function getUnpresentedSnapEffects(model, effects) {
79
+ const presentedEffects = getPresentedSnapEffects(model);
80
+ return normalizeEffects(effects).filter((effect) => !presentedEffects.has(effect));
81
+ }
82
+ export function markSnapEffectsPresented(model, effects) {
83
+ const nextEffects = normalizeEffects(effects);
84
+ if (nextEffects.length === 0)
85
+ return false;
86
+ const presentedEffects = getPresentedSnapEffects(model);
87
+ let changed = false;
88
+ for (const effect of nextEffects) {
89
+ if (!presentedEffects.has(effect)) {
90
+ presentedEffects.add(effect);
91
+ changed = true;
92
+ }
93
+ }
94
+ if (!changed)
95
+ return false;
96
+ let meta = getRenderStateMeta(model);
97
+ if (!meta) {
98
+ meta = {};
99
+ model[SNAP_RENDER_STATE_META_KEY] = meta;
100
+ }
101
+ meta.presentedEffects = Array.from(presentedEffects);
102
+ return true;
103
+ }
104
+ export function buildInitialRenderState({ specState, initialRenderState, themeAccent, }) {
105
+ const authoredState = isRecord(specState) ? specState : {};
106
+ const restoredState = initialRenderState
107
+ ? mergeRenderState(authoredState, initialRenderState)
108
+ : cloneSnapRenderState(authoredState);
109
+ if (!isRecord(restoredState.inputs)) {
110
+ restoredState.inputs = {};
111
+ }
112
+ const theme = isRecord(restoredState.theme) ? restoredState.theme : {};
113
+ restoredState.theme =
114
+ themeAccent === undefined ? theme : { ...theme, accent: themeAccent };
115
+ return restoredState;
116
+ }
@@ -572,6 +572,33 @@ export declare const snapJsonRenderCatalog: import("@json-render/core").Catalog<
572
572
  buyToken: z.ZodOptional<z.ZodString>;
573
573
  }, z.core.$strip>;
574
574
  };
575
+ send_transaction: {
576
+ description: string;
577
+ params: z.ZodObject<{
578
+ chainId: z.ZodString;
579
+ to: z.ZodString;
580
+ data: z.ZodOptional<z.ZodString>;
581
+ value: z.ZodOptional<z.ZodString>;
582
+ gas: z.ZodOptional<z.ZodString>;
583
+ gasPrice: z.ZodOptional<z.ZodString>;
584
+ maxFeePerGas: z.ZodOptional<z.ZodString>;
585
+ maxPriorityFeePerGas: z.ZodOptional<z.ZodString>;
586
+ }, z.core.$strip>;
587
+ };
588
+ send_calls: {
589
+ description: string;
590
+ params: z.ZodObject<{
591
+ version: z.ZodOptional<z.ZodLiteral<"1.0">>;
592
+ chainId: z.ZodString;
593
+ atomicRequired: z.ZodOptional<z.ZodBoolean>;
594
+ id: z.ZodOptional<z.ZodString>;
595
+ calls: z.ZodArray<z.ZodObject<{
596
+ to: z.ZodOptional<z.ZodString>;
597
+ data: z.ZodOptional<z.ZodString>;
598
+ value: z.ZodOptional<z.ZodString>;
599
+ }, z.core.$strip>>;
600
+ }, z.core.$strip>;
601
+ };
575
602
  paginator_next: {
576
603
  description: string;
577
604
  params: z.ZodObject<{
@@ -151,6 +151,33 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
151
151
  buyToken: z.string().optional(),
152
152
  }),
153
153
  },
154
+ send_transaction: {
155
+ description: "Request an EVM transaction through the host wallet using eth_sendTransaction.",
156
+ params: z.object({
157
+ chainId: z.string(),
158
+ to: z.string(),
159
+ data: z.string().optional(),
160
+ value: z.string().optional(),
161
+ gas: z.string().optional(),
162
+ gasPrice: z.string().optional(),
163
+ maxFeePerGas: z.string().optional(),
164
+ maxPriorityFeePerGas: z.string().optional(),
165
+ }),
166
+ },
167
+ send_calls: {
168
+ description: "Request one or more EVM calls through the host wallet using wallet_sendCalls.",
169
+ params: z.object({
170
+ version: z.literal("1.0").optional(),
171
+ chainId: z.string(),
172
+ atomicRequired: z.boolean().optional(),
173
+ id: z.string().optional(),
174
+ calls: z.array(z.object({
175
+ to: z.string().optional(),
176
+ data: z.string().optional(),
177
+ value: z.string().optional(),
178
+ })),
179
+ }),
180
+ },
154
181
  paginator_next: {
155
182
  description: "Move the snap's paginator to the next page locally. Does not POST and is ignored when no paginator is rendered.",
156
183
  params: z.object({ page: z.number().int().min(0).optional() }),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "2.6.4",
3
+ "version": "2.7.1",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -47,3 +47,4 @@ export {
47
47
  type SnapGetPayload,
48
48
  } from "./schemas";
49
49
  export { validateSnapResponse, type ValidationResult } from "./validator";
50
+ export type { SnapRenderState } from "./render-state";