@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 +1 -0
- package/dist/react/index.d.ts +30 -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 +93 -57
- 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 +93 -68
- package/dist/react-native/types.d.ts +25 -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/dist/ui/catalog.d.ts +27 -0
- package/dist/ui/catalog.js +27 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/react/index.tsx +38 -0
- package/src/react/snap-view-core.tsx +134 -62
- 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 +126 -77
- package/src/react-native/types.ts +28 -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
- package/src/ui/catalog.ts +31 -0
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
|
108
|
-
const
|
|
109
|
-
const
|
|
110
|
-
const
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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
|
+
}
|
package/dist/ui/catalog.d.ts
CHANGED
|
@@ -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<{
|
package/dist/ui/catalog.js
CHANGED
|
@@ -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
package/src/index.ts
CHANGED