@farcaster/snap 2.6.4 → 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.
- package/dist/index.d.ts +1 -0
- package/dist/react/index.d.ts +7 -1
- package/dist/react/index.js +3 -3
- package/dist/react/snap-view-core.d.ts +4 -5
- package/dist/react/snap-view-core.js +54 -58
- package/dist/react/v1/snap-view.d.ts +9 -3
- package/dist/react/v1/snap-view.js +4 -4
- package/dist/react/v2/snap-view.d.ts +9 -3
- package/dist/react/v2/snap-view.js +4 -4
- package/dist/react-native/index.d.ts +7 -3
- package/dist/react-native/index.js +3 -3
- package/dist/react-native/snap-view-core.d.ts +4 -5
- package/dist/react-native/snap-view-core.js +54 -69
- package/dist/react-native/types.d.ts +2 -0
- package/dist/react-native/v1/snap-view.d.ts +12 -4
- package/dist/react-native/v1/snap-view.js +8 -8
- package/dist/react-native/v2/snap-view.d.ts +12 -4
- package/dist/react-native/v2/snap-view.js +8 -8
- package/dist/render-state.d.ts +14 -0
- package/dist/render-state.js +116 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/react/index.tsx +13 -0
- package/src/react/snap-view-core.tsx +94 -65
- package/src/react/v1/snap-view.tsx +15 -1
- package/src/react/v2/snap-view.tsx +15 -1
- package/src/react-native/index.tsx +22 -2
- package/src/react-native/snap-view-core.tsx +86 -80
- package/src/react-native/types.ts +3 -0
- package/src/react-native/v1/snap-view.tsx +27 -1
- package/src/react-native/v2/snap-view.tsx +27 -1
- package/src/render-state.ts +184 -0
|
@@ -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,
|
|
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 {
|
|
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 {
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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 &&
|
|
255
|
-
|
|
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
|
>
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
SnapViewCoreInner,
|
|
7
7
|
resolveAccentHex,
|
|
8
8
|
} from "../snap-view-core";
|
|
9
|
-
import type { SnapPage,
|
|
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
|
);
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
validateSnapResponse,
|
|
12
12
|
type ValidationResult,
|
|
13
13
|
} from "@farcaster/snap";
|
|
14
|
-
import type { SnapPage,
|
|
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
|
|
|
@@ -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
|
);
|