@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.
package/dist/index.d.ts CHANGED
@@ -3,3 +3,4 @@ export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS,
3
3
  export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, isSnapHexColorString, readableTextOnHex, resolveSnapColorHex, type PaletteColor, } from "./colors.js";
4
4
  export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, getPayloadSchema, type SnapAction, type SnapGetAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, type SnapGetPayload, } from "./schemas.js";
5
5
  export { validateSnapResponse, type ValidationResult } from "./validator.js";
6
+ export type { SnapRenderState } from "./render-state.js";
@@ -1,6 +1,7 @@
1
1
  import type { Spec } from "@json-render/core";
2
2
  import type { ReactNode } from "react";
3
3
  import type { ValidationResult } from "../validator.js";
4
+ import type { SnapRenderState } from "../render-state.js";
4
5
  export type JsonValue = string | number | boolean | null | JsonValue[] | {
5
6
  [key: string]: JsonValue;
6
7
  };
@@ -12,6 +13,27 @@ export type SnapPage = {
12
13
  effects?: string[];
13
14
  ui: Spec;
14
15
  };
16
+ export type SnapSendTransactionParams = {
17
+ chainId: string;
18
+ to: string;
19
+ data?: string;
20
+ value?: string;
21
+ gas?: string;
22
+ gasPrice?: string;
23
+ maxFeePerGas?: string;
24
+ maxPriorityFeePerGas?: string;
25
+ };
26
+ export type SnapSendCallsParams = {
27
+ version?: "1.0";
28
+ chainId: string;
29
+ atomicRequired?: boolean;
30
+ id?: string;
31
+ calls: Array<{
32
+ to?: string;
33
+ data?: string;
34
+ value?: string;
35
+ }>;
36
+ };
15
37
  export type SnapActionHandlers = {
16
38
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
17
39
  open_url: (target: string) => void;
@@ -41,8 +63,11 @@ export type SnapActionHandlers = {
41
63
  sellToken?: string;
42
64
  buyToken?: string;
43
65
  }) => void;
66
+ send_transaction?: (params: SnapSendTransactionParams) => void;
67
+ send_calls?: (params: SnapSendCallsParams) => void;
44
68
  };
45
- export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
69
+ export type { SnapRenderState };
70
+ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, initialRenderState, onRenderStateChange, }: {
46
71
  snap: SnapPage;
47
72
  handlers: SnapActionHandlers;
48
73
  loading?: boolean;
@@ -58,4 +83,8 @@ export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth
58
83
  plain?: boolean;
59
84
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
60
85
  loadingOverlay?: ReactNode;
86
+ /** JSON-render local state used to seed this presenter mount. */
87
+ initialRenderState?: SnapRenderState;
88
+ /** Called with the full JSON-render local state after state changes. */
89
+ onRenderStateChange?: (state: SnapRenderState) => void;
61
90
  }): import("react/jsx-runtime").JSX.Element;
@@ -4,9 +4,9 @@ import { SPEC_VERSION_2 } from "../constants.js";
4
4
  import { SnapCardV1 } from "./v1/snap-view.js";
5
5
  import { SnapCardV2 } from "./v2/snap-view.js";
6
6
  // ─── SnapCard ────────────────────────────────────────
7
- export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
7
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, initialRenderState, onRenderStateChange, }) {
8
8
  if (snap.version === SPEC_VERSION_2) {
9
- return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
9
+ return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
10
10
  }
11
- return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay }));
11
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, maxWidth: maxWidth, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
12
12
  }
@@ -1,15 +1,12 @@
1
+ import { type SnapRenderState } from "../render-state.js";
1
2
  import { type ReactNode } from "react";
2
3
  import type { SnapActionHandlers, SnapPage } from "./index.js";
3
- export declare function applyStatePaths(model: Record<string, unknown>, changes: {
4
- path: string;
5
- value: unknown;
6
- }[] | Record<string, unknown> | null | undefined): void;
7
4
  export declare function SnapLoadingOverlay({ appearance, accentHex, active, }: {
8
5
  appearance: "light" | "dark";
9
6
  accentHex: string;
10
7
  active: boolean;
11
8
  }): import("react/jsx-runtime").JSX.Element;
12
- export declare function SnapViewCore({ snap, handlers, loading, appearance, loadingOverlay, }: {
9
+ export declare function SnapViewCore({ snap, handlers, loading, appearance, loadingOverlay, initialRenderState, onRenderStateChange, }: {
13
10
  snap: SnapPage;
14
11
  handlers: SnapActionHandlers;
15
12
  loading?: boolean;
@@ -19,4 +16,6 @@ export declare function SnapViewCore({ snap, handlers, loading, appearance, load
19
16
  * the built-in spinner + backdrop is used. Pass `null` to render nothing.
20
17
  */
21
18
  loadingOverlay?: ReactNode;
19
+ initialRenderState?: SnapRenderState;
20
+ onRenderStateChange?: (state: SnapRenderState) => void;
22
21
  }): import("react/jsx-runtime").JSX.Element;
@@ -6,40 +6,15 @@ import { SnapPreviewAccentProvider } from "./accent-context.js";
6
6
  import { SnapVersionProvider } from "./snap-version-context.js";
7
7
  import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex.js";
8
8
  import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css.js";
9
+ import { applyStatePaths, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, markSnapEffectsPresented, } from "../render-state.js";
9
10
  import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
10
- // ─── Internal helpers ──────────────────────────────────
11
- export function applyStatePaths(model, changes) {
12
- if (!changes)
13
- return;
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
- }
42
- }
11
+ function asRecord(value) {
12
+ return value && typeof value === "object"
13
+ ? value
14
+ : {};
15
+ }
16
+ function optionalString(value) {
17
+ return value ? String(value) : undefined;
43
18
  }
44
19
  function withDefaultElementProps(spec) {
45
20
  if (!spec || typeof spec !== "object" || !("elements" in spec))
@@ -102,7 +77,7 @@ function ConfettiOverlay() {
102
77
  overflow: "hidden",
103
78
  pointerEvents: "none",
104
79
  zIndex: 20,
105
- }, children: [pieces.map(({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid }) => (_jsx("div", { style: {
80
+ }, children: [pieces.map(({ id, left, delay, duration, color, size, rotation, isCircle, driftX, driftMid, }) => (_jsx("div", { style: {
106
81
  position: "absolute",
107
82
  left: `${left}%`,
108
83
  top: -20,
@@ -199,9 +174,7 @@ export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
199
174
  zIndex: 10,
200
175
  background: tint,
201
176
  backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
202
- WebkitBackdropFilter: active
203
- ? "blur(10px) saturate(1.05)"
204
- : "none",
177
+ WebkitBackdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
205
178
  opacity: active ? 1 : 0,
206
179
  pointerEvents: active ? "auto" : "none",
207
180
  transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
@@ -239,19 +212,16 @@ const PALETTE = [
239
212
  ];
240
213
  // ─── SnapViewCore ────────────────────────────────────
241
214
  // Shared rendering logic used by both v1 and v2.
242
- export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, }) {
215
+ export function SnapViewCore({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, initialRenderState, onRenderStateChange, }) {
243
216
  const spec = useMemo(() => withDefaultElementProps(snap.ui), [snap.ui]);
244
- const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
217
+ const initialState = useMemo(() => buildInitialRenderState({
218
+ specState: spec.state,
219
+ initialRenderState,
220
+ themeAccent: snap.theme?.accent,
221
+ }), [initialRenderState, spec.state, snap.theme?.accent]);
245
222
  const stateRef = useRef(initialState);
246
223
  useEffect(() => {
247
- stateRef.current = {
248
- inputs: {
249
- ...(initialState.inputs ?? {}),
250
- },
251
- theme: {
252
- ...(initialState.theme ?? {}),
253
- },
254
- };
224
+ stateRef.current = cloneSnapRenderState(initialState);
255
225
  }, [initialState]);
256
226
  useEffect(() => {
257
227
  const catalogResult = snapJsonRenderCatalog.validate(spec);
@@ -264,16 +234,49 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
264
234
  useEffect(() => {
265
235
  setPageKey((k) => k + 1);
266
236
  }, [spec]);
267
- const showConfetti = snap.effects?.includes("confetti") ?? false;
268
- const showFireworks = snap.effects?.includes("fireworks") ?? false;
269
- const [confettiKey, setConfettiKey] = useState(0);
270
- const [fireworksKey, setFireworksKey] = useState(0);
237
+ const effectSignature = snap.effects?.join("\u0000") ?? "";
238
+ const snapEffects = useMemo(() => (effectSignature ? effectSignature.split("\u0000") : []), [effectSignature]);
239
+ const showConfetti = snapEffects.includes("confetti");
240
+ const showFireworks = snapEffects.includes("fireworks");
241
+ const [effectRunKeys, setEffectRunKeys] = useState({
242
+ confetti: 0,
243
+ fireworks: 0,
244
+ });
245
+ const onRenderStateChangeRef = useRef(onRenderStateChange);
246
+ useEffect(() => {
247
+ onRenderStateChangeRef.current = onRenderStateChange;
248
+ }, [onRenderStateChange]);
271
249
  useEffect(() => {
272
- if (showConfetti)
273
- setConfettiKey((k) => k + 1);
274
- if (showFireworks)
275
- setFireworksKey((k) => k + 1);
276
- }, [showConfetti, showFireworks, snap]);
250
+ const effectsToPresent = getUnpresentedSnapEffects(stateRef.current, snapEffects);
251
+ if (effectsToPresent.length === 0) {
252
+ setEffectRunKeys((current) => {
253
+ const next = {
254
+ confetti: showConfetti ? current.confetti : 0,
255
+ fireworks: showFireworks ? current.fireworks : 0,
256
+ };
257
+ return next.confetti === current.confetti &&
258
+ next.fireworks === current.fireworks
259
+ ? current
260
+ : next;
261
+ });
262
+ return;
263
+ }
264
+ if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
265
+ onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
266
+ }
267
+ setEffectRunKeys((current) => ({
268
+ confetti: effectsToPresent.includes("confetti")
269
+ ? current.confetti + 1
270
+ : showConfetti
271
+ ? current.confetti
272
+ : 0,
273
+ fireworks: effectsToPresent.includes("fireworks")
274
+ ? current.fireworks + 1
275
+ : showFireworks
276
+ ? current.fireworks
277
+ : 0,
278
+ }));
279
+ }, [initialState, showConfetti, showFireworks, snapEffects]);
277
280
  const accentName = snap.theme?.accent ?? "purple";
278
281
  const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
279
282
  const previewSurfaceStyle = useMemo(() => {
@@ -335,11 +338,44 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
335
338
  buyToken: p.buyToken ? String(p.buyToken) : undefined,
336
339
  });
337
340
  break;
341
+ case "send_transaction":
342
+ handlers.send_transaction?.({
343
+ chainId: String(p.chainId ?? ""),
344
+ to: String(p.to ?? ""),
345
+ data: optionalString(p.data),
346
+ value: optionalString(p.value),
347
+ gas: optionalString(p.gas),
348
+ gasPrice: optionalString(p.gasPrice),
349
+ maxFeePerGas: optionalString(p.maxFeePerGas),
350
+ maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
351
+ });
352
+ break;
353
+ case "send_calls":
354
+ handlers.send_calls?.({
355
+ version: p.version === "1.0" ? "1.0" : undefined,
356
+ chainId: String(p.chainId ?? ""),
357
+ atomicRequired: typeof p.atomicRequired === "boolean"
358
+ ? p.atomicRequired
359
+ : undefined,
360
+ id: optionalString(p.id),
361
+ calls: Array.isArray(p.calls)
362
+ ? p.calls.map((call) => {
363
+ const c = asRecord(call);
364
+ return {
365
+ to: optionalString(c.to),
366
+ data: optionalString(c.data),
367
+ value: optionalString(c.value),
368
+ };
369
+ })
370
+ : [],
371
+ });
372
+ break;
338
373
  default:
339
374
  break;
340
375
  }
341
376
  }, [handlers]);
342
- return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && _jsx(ConfettiOverlay, {}, confettiKey), showFireworks && _jsx(FireworksOverlay, {}, fireworksKey), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
377
+ return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks)), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
343
378
  applyStatePaths(stateRef.current, changes);
379
+ onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
344
380
  }, onAction: handleAction }, pageKey) }) }) })] }));
345
381
  }
@@ -1,14 +1,16 @@
1
1
  import { type ReactNode } from "react";
2
- import type { SnapPage, SnapActionHandlers } from "../index.js";
3
- export declare function SnapViewV1({ snap, handlers, loading, appearance, loadingOverlay, }: {
2
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../index.js";
3
+ export declare function SnapViewV1({ snap, handlers, loading, appearance, loadingOverlay, initialRenderState, onRenderStateChange, }: {
4
4
  snap: SnapPage;
5
5
  handlers: SnapActionHandlers;
6
6
  loading?: boolean;
7
7
  appearance?: "light" | "dark";
8
8
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
9
9
  loadingOverlay?: ReactNode;
10
+ initialRenderState?: SnapRenderState;
11
+ onRenderStateChange?: (state: SnapRenderState) => void;
10
12
  }): import("react/jsx-runtime").JSX.Element;
11
- export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, loadingOverlay, }: {
13
+ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWidth, actionError, plain, loadingOverlay, initialRenderState, onRenderStateChange, }: {
12
14
  snap: SnapPage;
13
15
  handlers: SnapActionHandlers;
14
16
  loading?: boolean;
@@ -18,4 +20,8 @@ export declare function SnapCardV1({ snap, handlers, loading, appearance, maxWid
18
20
  plain?: boolean;
19
21
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
20
22
  loadingOverlay?: ReactNode;
23
+ /** JSON-render local state used to seed this presenter mount. */
24
+ initialRenderState?: SnapRenderState;
25
+ /** Called with the full JSON-render local state after state changes. */
26
+ onRenderStateChange?: (state: SnapRenderState) => void;
21
27
  }): import("react/jsx-runtime").JSX.Element;
@@ -4,10 +4,10 @@ import { useEffect, useMemo, useRef, useState } from "react";
4
4
  import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core.js";
5
5
  import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex.js";
6
6
  const SNAP_MAX_HEIGHT = 500;
7
- export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, }) {
8
- return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay }));
7
+ export function SnapViewV1({ snap, handlers, loading = false, appearance = "dark", loadingOverlay, initialRenderState, onRenderStateChange, }) {
8
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
9
9
  }
10
- export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, loadingOverlay, }) {
10
+ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, actionError, plain = false, loadingOverlay, initialRenderState, onRenderStateChange, }) {
11
11
  const isDark = appearance === "dark";
12
12
  const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
13
13
  const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
@@ -62,7 +62,7 @@ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark
62
62
  maxHeight: SNAP_MAX_HEIGHT,
63
63
  overflow: "hidden",
64
64
  }
65
- : undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null] }), isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
65
+ : undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: null, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null] }), isExpandable ? (_jsx("button", { type: "button", "aria-expanded": isExpanded, onClick: () => setIsExpanded((value) => !value), style: {
66
66
  position: "absolute",
67
67
  bottom: 0,
68
68
  left: "50%",
@@ -1,7 +1,7 @@
1
1
  import { type ReactNode } from "react";
2
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, loadingOverlay, }: {
3
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../index.js";
4
+ export declare function SnapViewV2({ snap, handlers, loading, appearance, onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }: {
5
5
  snap: SnapPage;
6
6
  handlers: SnapActionHandlers;
7
7
  loading?: boolean;
@@ -10,8 +10,10 @@ export declare function SnapViewV2({ snap, handlers, loading, appearance, onVali
10
10
  validationErrorFallback?: ReactNode;
11
11
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
12
12
  loadingOverlay?: ReactNode;
13
+ initialRenderState?: SnapRenderState;
14
+ onRenderStateChange?: (state: SnapRenderState) => void;
13
15
  }): import("react/jsx-runtime").JSX.Element | null;
14
- export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, }: {
16
+ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, initialRenderState, onRenderStateChange, }: {
15
17
  snap: SnapPage;
16
18
  handlers: SnapActionHandlers;
17
19
  loading?: boolean;
@@ -24,4 +26,8 @@ export declare function SnapCardV2({ snap, handlers, loading, appearance, maxWid
24
26
  plain?: boolean;
25
27
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
26
28
  loadingOverlay?: ReactNode;
29
+ /** JSON-render local state used to seed this presenter mount. */
30
+ initialRenderState?: SnapRenderState;
31
+ /** Called with the full JSON-render local state after state changes. */
32
+ onRenderStateChange?: (state: SnapRenderState) => void;
27
33
  }): import("react/jsx-runtime").JSX.Element;
@@ -21,7 +21,7 @@ function SnapValidationFallback({ appearance, message, }) {
21
21
  }, children: _jsx("span", { children: message ? `Unable to render snap: ${message}` : "Unable to render snap" }) }));
22
22
  }
23
23
  // ─── SnapViewV2 ──────────────────────────────────────
24
- export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, loadingOverlay, }) {
24
+ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark", onValidationError, validationErrorFallback, loadingOverlay, initialRenderState, onRenderStateChange, }) {
25
25
  const validation = useMemo(() => validateSnapResponse(snap), [snap]);
26
26
  const valid = validation.valid;
27
27
  const validationMessage = validation.issues[0]?.message;
@@ -41,10 +41,10 @@ export function SnapViewV2({ snap, handlers, loading = false, appearance = "dark
41
41
  return null;
42
42
  return _jsx(_Fragment, { children: validationErrorFallback ?? _jsx(SnapValidationFallback, { appearance: appearance, message: validationMessage }) });
43
43
  }
44
- return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay }));
44
+ return (_jsx(SnapViewCore, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, loadingOverlay: loadingOverlay, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
45
45
  }
46
46
  // ─── SnapCardV2 ──────────────────────────────────────
47
- export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, }) {
47
+ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark", maxWidth = 480, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, initialRenderState, onRenderStateChange, }) {
48
48
  const isDark = appearance === "dark";
49
49
  const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
50
50
  const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
@@ -101,7 +101,7 @@ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark
101
101
  }),
102
102
  }, children: [_jsx("div", { style: isClipped
103
103
  ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" }
104
- : undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, showOverflowWarning && (_jsxs("div", { style: {
104
+ : undefined, children: _jsx("div", { ref: contentRef, style: plain ? undefined : { padding: 16 }, children: _jsx(SnapViewV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, loadingOverlay: null, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }) }) }), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, showOverflowWarning && (_jsxs("div", { style: {
105
105
  position: "absolute",
106
106
  top: SNAP_MAX_HEIGHT,
107
107
  left: 0,
@@ -1,13 +1,13 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { ValidationResult } from "@farcaster/snap";
3
3
  import type { SnapNativeColors } from "./theme.js";
4
- import type { SnapPage, SnapActionHandlers } from "./types.js";
4
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "./types.js";
5
5
  import { useSnapTheme } from "./theme.js";
6
6
  import { hexToRgba } from "./use-snap-palette.js";
7
- export type { JsonValue, SnapPage, SnapActionHandlers } from "./types.js";
7
+ export type { JsonValue, SnapActionHandlers, SnapPage, SnapRenderState, } from "./types.js";
8
8
  export { useSnapTheme, hexToRgba };
9
9
  export type { SnapNativeColors };
10
- export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }: {
10
+ export declare function SnapCard({ snap, handlers, loading, appearance, colors, borderRadius, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }: {
11
11
  snap: SnapPage;
12
12
  handlers: SnapActionHandlers;
13
13
  loading?: boolean;
@@ -33,4 +33,8 @@ export declare function SnapCard({ snap, handlers, loading, appearance, colors,
33
33
  expandButtonLabel?: string;
34
34
  /** Called from the collapsed expand button instead of toggling internal state. */
35
35
  onExpandPress?: () => void;
36
+ /** JSON-render local state used to seed this presenter mount. */
37
+ initialRenderState?: SnapRenderState;
38
+ /** Called with the full JSON-render local state after state changes. */
39
+ onRenderStateChange?: (state: SnapRenderState) => void;
36
40
  }): import("react").JSX.Element;
@@ -7,9 +7,9 @@ import { SnapCardV2 } from "./v2/snap-view.js";
7
7
  // ─── Re-exports ───────────────────────────────────────
8
8
  export { useSnapTheme, hexToRgba };
9
9
  // ─── SnapCard (version-switching) ─────────────────────
10
- export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, }) {
10
+ export function SnapCard({ snap, handlers, loading = false, appearance = "dark", colors, borderRadius = 16, showOverflowWarning = false, onValidationError, validationErrorFallback, actionError, plain = false, loadingOverlay, forceExpanded, expandButtonLabel, onExpandPress, initialRenderState, onRenderStateChange, }) {
11
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, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }));
12
+ return (_jsx(SnapCardV2, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, showOverflowWarning: showOverflowWarning, onValidationError: onValidationError, validationErrorFallback: validationErrorFallback, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
13
13
  }
14
- return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress }));
14
+ return (_jsx(SnapCardV1, { snap: snap, handlers: handlers, loading: loading, appearance: appearance, colors: colors, borderRadius: borderRadius, actionError: actionError, plain: plain, loadingOverlay: loadingOverlay, forceExpanded: forceExpanded, expandButtonLabel: expandButtonLabel, onExpandPress: onExpandPress, initialRenderState: initialRenderState, onRenderStateChange: onRenderStateChange }));
15
15
  }
@@ -1,11 +1,8 @@
1
1
  import { type ReactNode } from "react";
2
+ import { type SnapRenderState } from "../render-state.js";
2
3
  import type { SnapPage, SnapActionHandlers } from "./types.js";
3
- export declare function applyStatePaths(model: Record<string, unknown>, changes: {
4
- path: string;
5
- value: unknown;
6
- }[] | Record<string, unknown> | null | undefined): void;
7
4
  export declare function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string;
8
- export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOverlay, }: {
5
+ export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOverlay, initialRenderState, onRenderStateChange, }: {
9
6
  snap: SnapPage;
10
7
  handlers: SnapActionHandlers;
11
8
  loading?: boolean;
@@ -14,6 +11,8 @@ export declare function SnapViewCoreInner({ snap, handlers, loading, loadingOver
14
11
  * the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
15
12
  */
16
13
  loadingOverlay?: ReactNode;
14
+ initialRenderState?: SnapRenderState;
15
+ onRenderStateChange?: (state: SnapRenderState) => void;
17
16
  }): import("react").JSX.Element;
18
17
  export declare function SnapLoadingOverlay({ appearance, accentHex, }: {
19
18
  appearance: "light" | "dark";