@farcaster/snap 2.6.3 → 2.7.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.
@@ -5,7 +5,7 @@ import { validateSnapResponse } from "../../validator.js";
5
5
  import type { ValidationResult } from "../../validator.js";
6
6
  import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
7
7
  import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
8
- import type { SnapPage, SnapActionHandlers } from "../index";
8
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../index";
9
9
 
10
10
  const SNAP_MAX_HEIGHT = 500;
11
11
  const SNAP_WARNING_HEIGHT = 700;
@@ -48,6 +48,8 @@ export function SnapViewV2({
48
48
  onValidationError,
49
49
  validationErrorFallback,
50
50
  loadingOverlay,
51
+ initialRenderState,
52
+ onRenderStateChange,
51
53
  }: {
52
54
  snap: SnapPage;
53
55
  handlers: SnapActionHandlers;
@@ -57,6 +59,8 @@ export function SnapViewV2({
57
59
  validationErrorFallback?: ReactNode;
58
60
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
59
61
  loadingOverlay?: ReactNode;
62
+ initialRenderState?: SnapRenderState;
63
+ onRenderStateChange?: (state: SnapRenderState) => void;
60
64
  }) {
61
65
  const validation = useMemo(() => validateSnapResponse(snap), [snap]);
62
66
  const valid = validation.valid;
@@ -85,6 +89,8 @@ export function SnapViewV2({
85
89
  loading={loading}
86
90
  appearance={appearance}
87
91
  loadingOverlay={loadingOverlay}
92
+ initialRenderState={initialRenderState}
93
+ onRenderStateChange={onRenderStateChange}
88
94
  />
89
95
  );
90
96
  }
@@ -103,6 +109,8 @@ export function SnapCardV2({
103
109
  actionError,
104
110
  plain = false,
105
111
  loadingOverlay,
112
+ initialRenderState,
113
+ onRenderStateChange,
106
114
  }: {
107
115
  snap: SnapPage;
108
116
  handlers: SnapActionHandlers;
@@ -116,6 +124,10 @@ export function SnapCardV2({
116
124
  plain?: boolean;
117
125
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
118
126
  loadingOverlay?: ReactNode;
127
+ /** JSON-render local state used to seed this presenter mount. */
128
+ initialRenderState?: SnapRenderState;
129
+ /** Called with the full JSON-render local state after state changes. */
130
+ onRenderStateChange?: (state: SnapRenderState) => void;
119
131
  }) {
120
132
  const isDark = appearance === "dark";
121
133
  const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
@@ -208,6 +220,8 @@ export function SnapCardV2({
208
220
  onValidationError={onValidationError}
209
221
  validationErrorFallback={validationErrorFallback}
210
222
  loadingOverlay={null}
223
+ initialRenderState={initialRenderState}
224
+ onRenderStateChange={onRenderStateChange}
211
225
  />
212
226
  </div>
213
227
  </div>
@@ -2,7 +2,12 @@ import type { ReactNode } from "react";
2
2
  import type { ValidationResult } from "@farcaster/snap";
3
3
  import { SPEC_VERSION_2 } from "@farcaster/snap";
4
4
  import type { SnapNativeColors } from "./theme";
5
- import type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
5
+ import type {
6
+ JsonValue,
7
+ SnapActionHandlers,
8
+ SnapPage,
9
+ SnapRenderState,
10
+ } from "./types";
6
11
  import { useSnapTheme } from "./theme";
7
12
  import { hexToRgba } from "./use-snap-palette";
8
13
  import { SnapCardV1 } from "./v1/snap-view";
@@ -10,7 +15,12 @@ import { SnapCardV2 } from "./v2/snap-view";
10
15
 
11
16
  // ─── Public types ──────────────────────────────────────
12
17
 
13
- export type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
18
+ export type {
19
+ JsonValue,
20
+ SnapActionHandlers,
21
+ SnapPage,
22
+ SnapRenderState,
23
+ } from "./types";
14
24
 
15
25
  // ─── Re-exports ───────────────────────────────────────
16
26
 
@@ -35,6 +45,8 @@ export function SnapCard({
35
45
  forceExpanded,
36
46
  expandButtonLabel,
37
47
  onExpandPress,
48
+ initialRenderState,
49
+ onRenderStateChange,
38
50
  }: {
39
51
  snap: SnapPage;
40
52
  handlers: SnapActionHandlers;
@@ -61,6 +73,10 @@ export function SnapCard({
61
73
  expandButtonLabel?: string;
62
74
  /** Called from the collapsed expand button instead of toggling internal state. */
63
75
  onExpandPress?: () => void;
76
+ /** JSON-render local state used to seed this presenter mount. */
77
+ initialRenderState?: SnapRenderState;
78
+ /** Called with the full JSON-render local state after state changes. */
79
+ onRenderStateChange?: (state: SnapRenderState) => void;
64
80
  }) {
65
81
  if (snap.version === SPEC_VERSION_2) {
66
82
  return (
@@ -80,6 +96,8 @@ export function SnapCard({
80
96
  forceExpanded={forceExpanded}
81
97
  expandButtonLabel={expandButtonLabel}
82
98
  onExpandPress={onExpandPress}
99
+ initialRenderState={initialRenderState}
100
+ onRenderStateChange={onRenderStateChange}
83
101
  />
84
102
  );
85
103
  }
@@ -98,6 +116,8 @@ export function SnapCard({
98
116
  forceExpanded={forceExpanded}
99
117
  expandButtonLabel={expandButtonLabel}
100
118
  onExpandPress={onExpandPress}
119
+ initialRenderState={initialRenderState}
120
+ onRenderStateChange={onRenderStateChange}
101
121
  />
102
122
  );
103
123
  }
@@ -20,49 +20,16 @@ import {
20
20
  PALETTE_DARK_HEX,
21
21
  type PaletteColor,
22
22
  } from "@farcaster/snap";
23
+ import {
24
+ applyStatePaths,
25
+ buildInitialRenderState,
26
+ cloneSnapRenderState,
27
+ getUnpresentedSnapEffects,
28
+ markSnapEffectsPresented,
29
+ type SnapRenderState,
30
+ } from "../render-state";
23
31
  import type { SnapPage, SnapActionHandlers, JsonValue } from "./types";
24
32
 
25
- // ─── Shared helpers ──────────────────────────────────
26
-
27
- export function applyStatePaths(
28
- model: Record<string, unknown>,
29
- changes:
30
- | { path: string; value: unknown }[]
31
- | Record<string, unknown>
32
- | null
33
- | undefined,
34
- ): void {
35
- if (!changes) return;
36
- const entries = Array.isArray(changes)
37
- ? changes.map((c) => [c.path, c.value] as const)
38
- : Object.entries(changes);
39
- for (const [path, value] of entries) {
40
- const trimmed = path.startsWith("/") ? path : `/${path}`;
41
- const parts = trimmed.split("/").filter(Boolean);
42
- if (parts.length < 2) continue;
43
- const [top, ...rest] = parts;
44
- if (top === "inputs") {
45
- if (typeof model.inputs !== "object" || model.inputs === null) {
46
- model.inputs = {};
47
- }
48
- const inputs = model.inputs as Record<string, unknown>;
49
- if (rest.length === 1) {
50
- inputs[rest[0]!] = value;
51
- }
52
- continue;
53
- }
54
- if (top === "theme") {
55
- if (typeof model.theme !== "object" || model.theme === null) {
56
- model.theme = {};
57
- }
58
- const theme = model.theme as Record<string, unknown>;
59
- if (rest.length === 1) {
60
- theme[rest[0]!] = value;
61
- }
62
- }
63
- }
64
- }
65
-
66
33
  function withDefaultElementProps(spec: Spec): Spec {
67
34
  if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
68
35
  const elements = spec.elements as unknown as Record<
@@ -106,6 +73,8 @@ export function SnapViewCoreInner({
106
73
  handlers,
107
74
  loading = false,
108
75
  loadingOverlay,
76
+ initialRenderState,
77
+ onRenderStateChange,
109
78
  }: {
110
79
  snap: SnapPage;
111
80
  handlers: SnapActionHandlers;
@@ -115,6 +84,8 @@ export function SnapViewCoreInner({
115
84
  * the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
116
85
  */
117
86
  loadingOverlay?: ReactNode;
87
+ initialRenderState?: SnapRenderState;
88
+ onRenderStateChange?: (state: SnapRenderState) => void;
118
89
  }) {
119
90
  const { mode } = useSnapTheme();
120
91
  const spec = useMemo(
@@ -124,28 +95,19 @@ export function SnapViewCoreInner({
124
95
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
125
96
 
126
97
  const initialState = useMemo(
127
- () => ({
128
- ...(spec.state ?? {}),
129
- inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
130
- theme: {
131
- ...((spec.state?.theme ?? {}) as Record<string, unknown>),
132
- ...(snap.theme ? { accent: snap.theme.accent } : {}),
133
- },
134
- }),
135
- [spec, snap.theme],
98
+ () =>
99
+ buildInitialRenderState({
100
+ specState: spec.state,
101
+ initialRenderState,
102
+ themeAccent: snap.theme?.accent,
103
+ }),
104
+ [initialRenderState, spec.state, snap.theme?.accent],
136
105
  );
137
106
 
138
107
  const stateRef = useRef<Record<string, unknown>>(initialState);
139
108
 
140
109
  useEffect(() => {
141
- stateRef.current = {
142
- inputs: {
143
- ...((initialState.inputs ?? {}) as Record<string, unknown>),
144
- },
145
- theme: {
146
- ...((initialState.theme ?? {}) as Record<string, unknown>),
147
- },
148
- };
110
+ stateRef.current = cloneSnapRenderState(initialState);
149
111
  }, [initialState]);
150
112
 
151
113
  useEffect(() => {
@@ -161,14 +123,58 @@ export function SnapViewCoreInner({
161
123
  setPageKey((k) => k + 1);
162
124
  }, [spec]);
163
125
 
164
- const showConfetti = snap.effects?.includes("confetti") ?? false;
165
- const showFireworks = snap.effects?.includes("fireworks") ?? false;
166
- const [confettiKey, setConfettiKey] = useState(0);
167
- const [fireworksKey, setFireworksKey] = useState(0);
126
+ const effectSignature = snap.effects?.join("\u0000") ?? "";
127
+ const snapEffects = useMemo(
128
+ () => (effectSignature ? effectSignature.split("\u0000") : []),
129
+ [effectSignature],
130
+ );
131
+ const showConfetti = snapEffects.includes("confetti");
132
+ const showFireworks = snapEffects.includes("fireworks");
133
+ const [effectRunKeys, setEffectRunKeys] = useState({
134
+ confetti: 0,
135
+ fireworks: 0,
136
+ });
137
+ const onRenderStateChangeRef = useRef(onRenderStateChange);
138
+ useEffect(() => {
139
+ onRenderStateChangeRef.current = onRenderStateChange;
140
+ }, [onRenderStateChange]);
168
141
  useEffect(() => {
169
- if (showConfetti) setConfettiKey((k) => k + 1);
170
- if (showFireworks) setFireworksKey((k) => k + 1);
171
- }, [showConfetti, showFireworks, snap]);
142
+ const effectsToPresent = getUnpresentedSnapEffects(
143
+ stateRef.current,
144
+ snapEffects,
145
+ );
146
+
147
+ if (effectsToPresent.length === 0) {
148
+ setEffectRunKeys((current) => {
149
+ const next = {
150
+ confetti: showConfetti ? current.confetti : 0,
151
+ fireworks: showFireworks ? current.fireworks : 0,
152
+ };
153
+ return next.confetti === current.confetti &&
154
+ next.fireworks === current.fireworks
155
+ ? current
156
+ : next;
157
+ });
158
+ return;
159
+ }
160
+
161
+ if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
162
+ onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
163
+ }
164
+
165
+ setEffectRunKeys((current) => ({
166
+ confetti: effectsToPresent.includes("confetti")
167
+ ? current.confetti + 1
168
+ : showConfetti
169
+ ? current.confetti
170
+ : 0,
171
+ fireworks: effectsToPresent.includes("fireworks")
172
+ ? current.fireworks + 1
173
+ : showFireworks
174
+ ? current.fireworks
175
+ : 0,
176
+ }));
177
+ }, [initialState, showConfetti, showFireworks, snapEffects]);
172
178
 
173
179
  const handlersRef = useRef(handlers);
174
180
  handlersRef.current = handlers;
@@ -229,16 +235,13 @@ export function SnapViewCoreInner({
229
235
 
230
236
  return (
231
237
  <View style={styles.container}>
232
- {loading
233
- ? loadingOverlay === undefined
234
- ? (
235
- <SnapLoadingOverlay
236
- appearance={mode}
237
- accentHex={accentHex}
238
- />
239
- )
240
- : loadingOverlay
241
- : null}
238
+ {loading ? (
239
+ loadingOverlay === undefined ? (
240
+ <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
241
+ ) : (
242
+ loadingOverlay
243
+ )
244
+ ) : null}
242
245
  <SnapVersionProvider value={snap.version === "2.0" ? "2.0" : "1.0"}>
243
246
  <SnapCatalogView
244
247
  key={pageKey}
@@ -247,12 +250,17 @@ export function SnapViewCoreInner({
247
250
  loading={false}
248
251
  onStateChange={(changes) => {
249
252
  applyStatePaths(stateRef.current, changes);
253
+ onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
250
254
  }}
251
255
  onAction={handleAction}
252
256
  />
253
257
  </SnapVersionProvider>
254
- {showConfetti && <ConfettiOverlay key={confettiKey} />}
255
- {showFireworks && <FireworksOverlay key={fireworksKey} />}
258
+ {showConfetti && effectRunKeys.confetti > 0 && (
259
+ <ConfettiOverlay key={effectRunKeys.confetti} />
260
+ )}
261
+ {showFireworks && effectRunKeys.fireworks > 0 && (
262
+ <FireworksOverlay key={effectRunKeys.fireworks} />
263
+ )}
256
264
  </View>
257
265
  );
258
266
  }
@@ -270,9 +278,7 @@ export function SnapLoadingOverlay({
270
278
  styles.overlay,
271
279
  {
272
280
  backgroundColor:
273
- appearance === "dark"
274
- ? "rgba(0,0,0,0.1)"
275
- : "rgba(255,255,255,0.2)",
281
+ appearance === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
276
282
  },
277
283
  ]}
278
284
  >
@@ -1,4 +1,7 @@
1
1
  import type { Spec } from "@json-render/core";
2
+ import type { SnapRenderState } from "../render-state";
3
+
4
+ export type { SnapRenderState };
2
5
 
3
6
  export type JsonValue =
4
7
  | string
@@ -6,7 +6,7 @@ import {
6
6
  SnapViewCoreInner,
7
7
  resolveAccentHex,
8
8
  } from "../snap-view-core";
9
- import type { SnapPage, SnapActionHandlers } from "../types";
9
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../types";
10
10
  import { getSnapExpansionState } from "../expand-state";
11
11
 
12
12
  // ─── SnapViewV1 (no validation) ──────────────────────
@@ -16,11 +16,15 @@ export function SnapViewV1Inner({
16
16
  handlers,
17
17
  loading = false,
18
18
  loadingOverlay,
19
+ initialRenderState,
20
+ onRenderStateChange,
19
21
  }: {
20
22
  snap: SnapPage;
21
23
  handlers: SnapActionHandlers;
22
24
  loading?: boolean;
23
25
  loadingOverlay?: ReactNode;
26
+ initialRenderState?: SnapRenderState;
27
+ onRenderStateChange?: (state: SnapRenderState) => void;
24
28
  }) {
25
29
  return (
26
30
  <SnapViewCoreInner
@@ -28,6 +32,8 @@ export function SnapViewV1Inner({
28
32
  handlers={handlers}
29
33
  loading={loading}
30
34
  loadingOverlay={loadingOverlay}
35
+ initialRenderState={initialRenderState}
36
+ onRenderStateChange={onRenderStateChange}
31
37
  />
32
38
  );
33
39
  }
@@ -39,6 +45,8 @@ export function SnapViewV1({
39
45
  appearance = "dark",
40
46
  colors,
41
47
  loadingOverlay,
48
+ initialRenderState,
49
+ onRenderStateChange,
42
50
  }: {
43
51
  snap: SnapPage;
44
52
  handlers: SnapActionHandlers;
@@ -47,6 +55,8 @@ export function SnapViewV1({
47
55
  colors?: Partial<SnapNativeColors>;
48
56
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
49
57
  loadingOverlay?: ReactNode;
58
+ initialRenderState?: SnapRenderState;
59
+ onRenderStateChange?: (state: SnapRenderState) => void;
50
60
  }) {
51
61
  return (
52
62
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -55,6 +65,8 @@ export function SnapViewV1({
55
65
  handlers={handlers}
56
66
  loading={loading}
57
67
  loadingOverlay={loadingOverlay}
68
+ initialRenderState={initialRenderState}
69
+ onRenderStateChange={onRenderStateChange}
58
70
  />
59
71
  </SnapThemeProvider>
60
72
  );
@@ -74,6 +86,8 @@ function SnapCardV1Inner({
74
86
  forceExpanded,
75
87
  expandButtonLabel,
76
88
  onExpandPress,
89
+ initialRenderState,
90
+ onRenderStateChange,
77
91
  }: {
78
92
  snap: SnapPage;
79
93
  handlers: SnapActionHandlers;
@@ -86,6 +100,8 @@ function SnapCardV1Inner({
86
100
  forceExpanded?: boolean;
87
101
  expandButtonLabel?: string;
88
102
  onExpandPress?: () => void;
103
+ initialRenderState?: SnapRenderState;
104
+ onRenderStateChange?: (state: SnapRenderState) => void;
89
105
  }) {
90
106
  const { colors, mode } = useSnapTheme();
91
107
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
@@ -148,6 +164,8 @@ function SnapCardV1Inner({
148
164
  handlers={handlers}
149
165
  loading={loading}
150
166
  loadingOverlay={null}
167
+ initialRenderState={initialRenderState}
168
+ onRenderStateChange={onRenderStateChange}
151
169
  />
152
170
  </View>
153
171
  </View>
@@ -223,6 +241,8 @@ export function SnapCardV1({
223
241
  forceExpanded,
224
242
  expandButtonLabel,
225
243
  onExpandPress,
244
+ initialRenderState,
245
+ onRenderStateChange,
226
246
  }: {
227
247
  snap: SnapPage;
228
248
  handlers: SnapActionHandlers;
@@ -240,6 +260,10 @@ export function SnapCardV1({
240
260
  expandButtonLabel?: string;
241
261
  /** Called from the collapsed expand button instead of toggling internal state. */
242
262
  onExpandPress?: () => void;
263
+ /** JSON-render local state used to seed this presenter mount. */
264
+ initialRenderState?: SnapRenderState;
265
+ /** Called with the full JSON-render local state after state changes. */
266
+ onRenderStateChange?: (state: SnapRenderState) => void;
243
267
  }) {
244
268
  return (
245
269
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -255,6 +279,8 @@ export function SnapCardV1({
255
279
  forceExpanded={forceExpanded}
256
280
  expandButtonLabel={expandButtonLabel}
257
281
  onExpandPress={onExpandPress}
282
+ initialRenderState={initialRenderState}
283
+ onRenderStateChange={onRenderStateChange}
258
284
  />
259
285
  </SnapThemeProvider>
260
286
  );
@@ -263,7 +289,7 @@ export function SnapCardV1({
263
289
  const cardStyles = StyleSheet.create({
264
290
  frameRing: { alignSelf: "stretch" },
265
291
  card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
266
- body: { paddingHorizontal: 16, paddingVertical: 16 },
292
+ body: { paddingHorizontal: 8, paddingVertical: 8 },
267
293
  expandFloat: {
268
294
  position: "absolute",
269
295
  left: 0,
@@ -11,7 +11,7 @@ import {
11
11
  validateSnapResponse,
12
12
  type ValidationResult,
13
13
  } from "@farcaster/snap";
14
- import type { SnapPage, SnapActionHandlers } from "../types";
14
+ import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../types";
15
15
  import { getSnapExpansionState, SNAP_MAX_HEIGHT } from "../expand-state";
16
16
 
17
17
  // ─── Constants ───────────────────────────────────────
@@ -53,6 +53,8 @@ export function SnapViewV2Inner({
53
53
  onValidationError,
54
54
  validationErrorFallback,
55
55
  loadingOverlay,
56
+ initialRenderState,
57
+ onRenderStateChange,
56
58
  }: {
57
59
  snap: SnapPage;
58
60
  handlers: SnapActionHandlers;
@@ -60,6 +62,8 @@ export function SnapViewV2Inner({
60
62
  onValidationError?: (result: ValidationResult) => void;
61
63
  validationErrorFallback?: ReactNode;
62
64
  loadingOverlay?: ReactNode;
65
+ initialRenderState?: SnapRenderState;
66
+ onRenderStateChange?: (state: SnapRenderState) => void;
63
67
  }) {
64
68
  const validation = useMemo(() => validateSnapResponse(snap), [snap]);
65
69
  const valid = validation.valid;
@@ -89,6 +93,8 @@ export function SnapViewV2Inner({
89
93
  handlers={handlers}
90
94
  loading={loading}
91
95
  loadingOverlay={loadingOverlay}
96
+ initialRenderState={initialRenderState}
97
+ onRenderStateChange={onRenderStateChange}
92
98
  />
93
99
  );
94
100
  }
@@ -102,6 +108,8 @@ export function SnapViewV2({
102
108
  onValidationError,
103
109
  validationErrorFallback,
104
110
  loadingOverlay,
111
+ initialRenderState,
112
+ onRenderStateChange,
105
113
  }: {
106
114
  snap: SnapPage;
107
115
  handlers: SnapActionHandlers;
@@ -112,6 +120,8 @@ export function SnapViewV2({
112
120
  validationErrorFallback?: ReactNode;
113
121
  /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
114
122
  loadingOverlay?: ReactNode;
123
+ initialRenderState?: SnapRenderState;
124
+ onRenderStateChange?: (state: SnapRenderState) => void;
115
125
  }) {
116
126
  return (
117
127
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -122,6 +132,8 @@ export function SnapViewV2({
122
132
  onValidationError={onValidationError}
123
133
  validationErrorFallback={validationErrorFallback}
124
134
  loadingOverlay={loadingOverlay}
135
+ initialRenderState={initialRenderState}
136
+ onRenderStateChange={onRenderStateChange}
125
137
  />
126
138
  </SnapThemeProvider>
127
139
  );
@@ -144,6 +156,8 @@ function SnapCardV2Inner({
144
156
  forceExpanded,
145
157
  expandButtonLabel,
146
158
  onExpandPress,
159
+ initialRenderState,
160
+ onRenderStateChange,
147
161
  }: {
148
162
  snap: SnapPage;
149
163
  handlers: SnapActionHandlers;
@@ -159,6 +173,8 @@ function SnapCardV2Inner({
159
173
  forceExpanded?: boolean;
160
174
  expandButtonLabel?: string;
161
175
  onExpandPress?: () => void;
176
+ initialRenderState?: SnapRenderState;
177
+ onRenderStateChange?: (state: SnapRenderState) => void;
162
178
  }) {
163
179
  const { colors, mode } = useSnapTheme();
164
180
  const accentHex = resolveAccentHex(snap.theme?.accent, mode);
@@ -188,6 +204,8 @@ function SnapCardV2Inner({
188
204
  onValidationError={onValidationError}
189
205
  validationErrorFallback={validationErrorFallback}
190
206
  loadingOverlay={null}
207
+ initialRenderState={initialRenderState}
208
+ onRenderStateChange={onRenderStateChange}
191
209
  />
192
210
  );
193
211
 
@@ -289,7 +307,7 @@ function SnapCardV2Inner({
289
307
  : nextHeight,
290
308
  );
291
309
  }}
292
- style={{ paddingHorizontal: 16, paddingVertical: 16 }}
310
+ style={cardStyles.body}
293
311
  >
294
312
  {content}
295
313
  </View>
@@ -375,6 +393,8 @@ export function SnapCardV2({
375
393
  forceExpanded,
376
394
  expandButtonLabel,
377
395
  onExpandPress,
396
+ initialRenderState,
397
+ onRenderStateChange,
378
398
  }: {
379
399
  snap: SnapPage;
380
400
  handlers: SnapActionHandlers;
@@ -395,6 +415,10 @@ export function SnapCardV2({
395
415
  expandButtonLabel?: string;
396
416
  /** Called from the collapsed expand button instead of toggling internal state. */
397
417
  onExpandPress?: () => void;
418
+ /** JSON-render local state used to seed this presenter mount. */
419
+ initialRenderState?: SnapRenderState;
420
+ /** Called with the full JSON-render local state after state changes. */
421
+ onRenderStateChange?: (state: SnapRenderState) => void;
398
422
  }) {
399
423
  return (
400
424
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -413,6 +437,8 @@ export function SnapCardV2({
413
437
  forceExpanded={forceExpanded}
414
438
  expandButtonLabel={expandButtonLabel}
415
439
  onExpandPress={onExpandPress}
440
+ initialRenderState={initialRenderState}
441
+ onRenderStateChange={onRenderStateChange}
416
442
  />
417
443
  </SnapThemeProvider>
418
444
  );
@@ -421,7 +447,7 @@ export function SnapCardV2({
421
447
  const cardStyles = StyleSheet.create({
422
448
  frameRing: { alignSelf: "stretch" },
423
449
  card: { borderWidth: 1, minHeight: 120, overflow: "hidden" },
424
- body: { paddingHorizontal: 16, paddingVertical: 16 },
450
+ body: { paddingHorizontal: 8, paddingVertical: 8 },
425
451
  actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
426
452
  expandFloat: {
427
453
  position: "absolute",