@farcaster/snap 2.9.0 → 2.10.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 -1
- package/dist/index.js +1 -1
- package/dist/react/catalog-renderer.d.ts +5 -5
- package/dist/react/catalog-renderer.js +16 -4
- package/dist/react/components/action-button.js +23 -5
- package/dist/react/index.d.ts +2 -1
- package/dist/react/snap-view-core.js +90 -25
- package/dist/react/v1/snap-view.js +1 -1
- package/dist/react/v2/snap-view.js +1 -1
- package/dist/react-native/components/snap-action-button.js +6 -1
- package/dist/react-native/snap-view-core.js +77 -24
- package/dist/react-native/types.d.ts +2 -1
- package/dist/render-state.d.ts +9 -0
- package/dist/render-state.js +27 -0
- package/dist/schemas.d.ts +123 -3
- package/dist/schemas.js +53 -2
- package/dist/server/parseRequest.js +19 -3
- package/dist/ui/button.d.ts +1 -0
- package/dist/ui/button.js +1 -0
- package/dist/ui/catalog.d.ts +13 -0
- package/dist/ui/catalog.js +15 -8
- package/package.json +1 -1
- package/src/index.ts +7 -0
- package/src/react/catalog-renderer.tsx +57 -3
- package/src/react/components/action-button.tsx +32 -3
- package/src/react/index.tsx +4 -1
- package/src/react/snap-view-core.tsx +144 -27
- package/src/react/v1/snap-view.tsx +1 -0
- package/src/react/v2/snap-view.tsx +1 -0
- package/src/react-native/components/snap-action-button.tsx +6 -1
- package/src/react-native/snap-view-core.tsx +114 -27
- package/src/react-native/types.ts +4 -1
- package/src/render-state.ts +46 -0
- package/src/schemas.ts +73 -2
- package/src/server/parseRequest.ts +37 -6
- package/src/ui/button.ts +1 -0
- package/src/ui/catalog.ts +16 -8
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export type { Spec as SnapSpec, UIElement as SnapUIElement, } from "@json-render/core";
|
|
2
2
|
export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, type SpecVersion, SNAP_PAYLOAD_HEADER, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
|
|
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
|
-
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";
|
|
4
|
+
export { ACTION_TYPE_GET, ACTION_TYPE_POST, ACTION_TYPE_TRANSACTION_RESULT, snapResponseSchema, payloadSchema, getPayloadSchema, transactionResultPayloadSchema, snapTransactionResultSchema, type SnapAction, type SnapGetAction, type SnapTransactionResultAction, type SnapContext, type SnapResponse, type SnapHandlerResult, type SnapElementInput, type SnapSpecInput, type SnapFunction, type SnapPayload, type SnapGetPayload, type SnapSendTransactionParams, type SnapTransactionResult, type SnapTransactionResultPayload, } from "./schemas.js";
|
|
5
5
|
export { validateSnapResponse, type ValidationResult } from "./validator.js";
|
|
6
6
|
export type { SnapRenderState } from "./render-state.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { SPEC_VERSION, SPEC_VERSION_1, SPEC_VERSION_2, SUPPORTED_SPEC_VERSIONS, SNAP_PAYLOAD_HEADER, MEDIA_TYPE, EFFECT_VALUES, POST_GRID_TAP_KEY, MAX_ELEMENTS, MAX_ROOT_CHILDREN, MAX_CHILDREN, MAX_DEPTH, } from "./constants.js";
|
|
2
2
|
export { DEFAULT_THEME_ACCENT, PALETTE_COLOR, PALETTE_COLOR_ACCENT, PALETTE_COLOR_VALUES, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, isSnapHexColorString, readableTextOnHex, resolveSnapColorHex, } from "./colors.js";
|
|
3
|
-
export { ACTION_TYPE_GET, ACTION_TYPE_POST, snapResponseSchema, payloadSchema, getPayloadSchema, } from "./schemas.js";
|
|
3
|
+
export { ACTION_TYPE_GET, ACTION_TYPE_POST, ACTION_TYPE_TRANSACTION_RESULT, snapResponseSchema, payloadSchema, getPayloadSchema, transactionResultPayloadSchema, snapTransactionResultSchema, } from "./schemas.js";
|
|
4
4
|
export { validateSnapResponse } from "./validator.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
import { type CreateRendererProps } from "@json-render/react";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export declare function SnapCatalogView({ spec, store, state, onAction, onStateChange, functions, loading, fallback, children, }: CreateRendererProps & {
|
|
4
|
+
children?: ReactNode;
|
|
5
|
+
}): import("react/jsx-runtime").JSX.Element;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { JSONUIProvider, Renderer, } from "@json-render/react";
|
|
4
|
+
import { useMemo } from "react";
|
|
4
5
|
import { SnapActionButton } from "./components/action-button.js";
|
|
5
6
|
import { SnapBadge } from "./components/badge.js";
|
|
6
7
|
import { SnapIcon } from "./components/icon.js";
|
|
@@ -22,7 +23,7 @@ import { SnapCellGrid } from "./components/cell-grid.js";
|
|
|
22
23
|
* Maps snap json-render catalog types to React components.
|
|
23
24
|
* Keys match the snap wire-format `type` strings exactly.
|
|
24
25
|
*/
|
|
25
|
-
|
|
26
|
+
const snapCatalogRegistry = {
|
|
26
27
|
badge: SnapBadge,
|
|
27
28
|
button: SnapActionButton,
|
|
28
29
|
icon: SnapIcon,
|
|
@@ -40,4 +41,15 @@ export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
|
|
|
40
41
|
toggle_group: SnapToggleGroup,
|
|
41
42
|
bar_chart: SnapBarChart,
|
|
42
43
|
cell_grid: SnapCellGrid,
|
|
43
|
-
}
|
|
44
|
+
};
|
|
45
|
+
export function SnapCatalogView({ spec, store, state, onAction, onStateChange, functions, loading, fallback, children, }) {
|
|
46
|
+
const actionHandlers = useMemo(() => onAction
|
|
47
|
+
? new Proxy({}, {
|
|
48
|
+
get: (_target, prop) => {
|
|
49
|
+
return (params) => onAction(String(prop), params);
|
|
50
|
+
},
|
|
51
|
+
has: () => true,
|
|
52
|
+
})
|
|
53
|
+
: undefined, [onAction]);
|
|
54
|
+
return (_jsxs(JSONUIProvider, { registry: snapCatalogRegistry, store: store, initialState: state, handlers: actionHandlers, functions: functions, onStateChange: onStateChange, children: [_jsx(Renderer, { spec: spec, registry: snapCatalogRegistry, loading: loading, fallback: fallback }), children] }));
|
|
55
|
+
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { useStateStore } from "@json-render/react";
|
|
3
|
+
import { useMemo, useState } from "react";
|
|
4
|
+
import { useStateStore, useStateValue } from "@json-render/react";
|
|
5
5
|
import { ExternalLink } from "lucide-react";
|
|
6
6
|
import { Button } from "@neynar/ui/button";
|
|
7
7
|
import { cn } from "@neynar/ui/utils";
|
|
8
8
|
import { useSnapColors } from "../hooks/use-snap-colors.js";
|
|
9
9
|
import { getPaginatorAction, runPaginatorAction, } from "../../ui/paginator-state.js";
|
|
10
|
+
import { buildActionActivityStateChanges } from "../../render-state.js";
|
|
10
11
|
import { useSnapStackDirection } from "../stack-direction-context.js";
|
|
11
12
|
import { ICON_MAP } from "./icon.js";
|
|
12
13
|
function isExternalLinkAction(on) {
|
|
@@ -17,21 +18,36 @@ function isExternalLinkAction(on) {
|
|
|
17
18
|
return false;
|
|
18
19
|
return press.action === "open_url";
|
|
19
20
|
}
|
|
21
|
+
function getActionPendingPath(on) {
|
|
22
|
+
const press = on?.press;
|
|
23
|
+
if (!press?.action)
|
|
24
|
+
return "/__snap/action/pending";
|
|
25
|
+
return (buildActionActivityStateChanges({
|
|
26
|
+
actionName: press.action,
|
|
27
|
+
params: press.params ?? {},
|
|
28
|
+
pending: true,
|
|
29
|
+
}).find((change) => change.path.endsWith("/pending"))?.path ??
|
|
30
|
+
"/__snap/action/pending");
|
|
31
|
+
}
|
|
20
32
|
export function SnapActionButton({ element, emit, }) {
|
|
21
33
|
const { props } = element;
|
|
22
34
|
const label = String(props.label ?? "Action");
|
|
23
35
|
const variant = String(props.variant ?? "secondary");
|
|
24
36
|
const isPrimary = variant === "primary";
|
|
37
|
+
const disabled = props.disabled === true;
|
|
25
38
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
26
39
|
const colors = useSnapColors();
|
|
27
40
|
const [hovered, setHovered] = useState(false);
|
|
28
41
|
const stateStore = useStateStore();
|
|
29
42
|
const paginatorAction = getPaginatorAction(element.on);
|
|
43
|
+
const actionPendingPath = useMemo(() => getActionPendingPath(element.on), [element.on]);
|
|
44
|
+
const actionPending = useStateValue(actionPendingPath) === true;
|
|
30
45
|
const Icon = iconName ? ICON_MAP[iconName] : undefined;
|
|
31
46
|
const showExternalIcon = isExternalLinkAction(element.on);
|
|
32
47
|
const inHorizontalStack = useSnapStackDirection() === "horizontal";
|
|
33
48
|
const style = {
|
|
34
|
-
cursor: "pointer",
|
|
49
|
+
cursor: disabled ? "not-allowed" : "pointer",
|
|
50
|
+
opacity: disabled ? 0.62 : 1,
|
|
35
51
|
...(isPrimary
|
|
36
52
|
? {
|
|
37
53
|
backgroundColor: hovered ? colors.accentHover : colors.accent,
|
|
@@ -54,9 +70,11 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
54
70
|
*/
|
|
55
71
|
_jsx("div", { className: inHorizontalStack
|
|
56
72
|
? "min-w-0 flex-auto"
|
|
57
|
-
: "w-full min-w-0", style: inHorizontalStack ? { flex: "1 1 auto" } : undefined, children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("h-8 w-full gap-2 px-3 text-sm"), style: style, onClick: () => {
|
|
73
|
+
: "w-full min-w-0", style: inHorizontalStack ? { flex: "1 1 auto" } : undefined, children: _jsxs(Button, { type: "button", variant: isPrimary ? "default" : "secondary", className: cn("h-8 w-full gap-2 px-3 text-sm"), disabled: disabled, style: style, onClick: () => {
|
|
74
|
+
if (disabled)
|
|
75
|
+
return;
|
|
58
76
|
if (!runPaginatorAction(stateStore, paginatorAction)) {
|
|
59
77
|
emit("press");
|
|
60
78
|
}
|
|
61
|
-
}, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
|
|
79
|
+
}, onPointerEnter: () => setHovered(true), onPointerLeave: () => setHovered(false), children: [Icon && _jsx(Icon, { size: 16 }), label, actionPending && (_jsx("span", { "data-snap-action-pending-active": "true", hidden: true })), showExternalIcon && (_jsx(ExternalLink, { size: 14, style: { opacity: 0.6 } }))] }) }));
|
|
62
80
|
}
|
package/dist/react/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { Spec } from "@json-render/core";
|
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import type { ValidationResult } from "../validator.js";
|
|
4
4
|
import type { SnapRenderState } from "../render-state.js";
|
|
5
|
+
import type { SnapTransactionResult } from "../schemas.js";
|
|
5
6
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
6
7
|
[key: string]: JsonValue;
|
|
7
8
|
};
|
|
@@ -55,7 +56,7 @@ export type SnapActionHandlers = {
|
|
|
55
56
|
sellToken?: string;
|
|
56
57
|
buyToken?: string;
|
|
57
58
|
}) => void;
|
|
58
|
-
send_transaction?: (params: SnapSendTransactionParams) => void
|
|
59
|
+
send_transaction?: (params: SnapSendTransactionParams) => void | Promise<void | SnapTransactionResult>;
|
|
59
60
|
};
|
|
60
61
|
export type { SnapRenderState };
|
|
61
62
|
export declare function SnapCard({ snap, handlers, loading, appearance, maxWidth, showOverflowWarning, onValidationError, validationErrorFallback, actionError, plain, loadingOverlay, initialRenderState, onRenderStateChange, }: {
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { createStateStore } from "@json-render/react";
|
|
3
4
|
import { snapJsonRenderCatalog } from "../ui/index.js";
|
|
4
5
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
5
6
|
import { SnapPreviewAccentProvider } from "./accent-context.js";
|
|
6
7
|
import { SnapVersionProvider } from "./snap-version-context.js";
|
|
7
8
|
import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex.js";
|
|
8
9
|
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css.js";
|
|
9
|
-
import {
|
|
10
|
+
import { buildActionActivityStateChanges, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, hasPendingSnapAction, markSnapEffectsPresented, } from "../render-state.js";
|
|
10
11
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
11
12
|
function asRecord(value) {
|
|
12
13
|
return value && typeof value === "object"
|
|
@@ -16,6 +17,11 @@ function asRecord(value) {
|
|
|
16
17
|
function optionalString(value) {
|
|
17
18
|
return value ? String(value) : undefined;
|
|
18
19
|
}
|
|
20
|
+
function recordValue(value) {
|
|
21
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
22
|
+
? value
|
|
23
|
+
: undefined;
|
|
24
|
+
}
|
|
19
25
|
function withDefaultElementProps(spec) {
|
|
20
26
|
if (!spec || typeof spec !== "object" || !("elements" in spec))
|
|
21
27
|
return spec;
|
|
@@ -165,7 +171,7 @@ export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
|
165
171
|
const trackColor = isDark
|
|
166
172
|
? "rgba(255, 255, 255, 0.12)"
|
|
167
173
|
: "rgba(15, 23, 42, 0.1)";
|
|
168
|
-
return (_jsxs("div", { style: {
|
|
174
|
+
return (_jsxs("div", { "data-snap-loading-overlay": true, "data-snap-loading-active": active ? "true" : "false", style: {
|
|
169
175
|
position: "absolute",
|
|
170
176
|
inset: 0,
|
|
171
177
|
display: "flex",
|
|
@@ -188,6 +194,20 @@ export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
|
188
194
|
animation: "snapViewSpin 0.75s linear infinite",
|
|
189
195
|
flexShrink: 0,
|
|
190
196
|
} }), _jsx("style", { children: `
|
|
197
|
+
[data-snap-view-root]:has([data-snap-action-pending-active="true"])
|
|
198
|
+
[data-snap-loading-overlay] {
|
|
199
|
+
opacity: 1 !important;
|
|
200
|
+
pointer-events: auto !important;
|
|
201
|
+
backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
202
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
203
|
+
}
|
|
204
|
+
[data-snap-card-surface]:has([data-snap-action-pending-active="true"])
|
|
205
|
+
> [data-snap-loading-overlay] {
|
|
206
|
+
opacity: 1 !important;
|
|
207
|
+
pointer-events: auto !important;
|
|
208
|
+
backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
209
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
210
|
+
}
|
|
191
211
|
@keyframes snapViewSpin {
|
|
192
212
|
to { transform: rotate(360deg); }
|
|
193
213
|
}
|
|
@@ -200,6 +220,9 @@ export function SnapLoadingOverlay({ appearance, accentHex, active, }) {
|
|
|
200
220
|
}
|
|
201
221
|
` })] }));
|
|
202
222
|
}
|
|
223
|
+
function SnapPendingActionOverlay({ appearance, accentHex, }) {
|
|
224
|
+
return (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: false }));
|
|
225
|
+
}
|
|
203
226
|
const PALETTE = [
|
|
204
227
|
"gray",
|
|
205
228
|
"blue",
|
|
@@ -219,10 +242,23 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
219
242
|
initialRenderState,
|
|
220
243
|
themeAccent: snap.theme?.accent,
|
|
221
244
|
}), [initialRenderState, spec.state, snap.theme?.accent]);
|
|
245
|
+
const stateStore = useMemo(() => createStateStore(initialState), [
|
|
246
|
+
initialState,
|
|
247
|
+
]);
|
|
222
248
|
const stateRef = useRef(initialState);
|
|
249
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
250
|
+
const pendingActionCountRef = useRef(0);
|
|
223
251
|
useEffect(() => {
|
|
224
252
|
stateRef.current = cloneSnapRenderState(initialState);
|
|
225
253
|
}, [initialState]);
|
|
254
|
+
useEffect(() => {
|
|
255
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
256
|
+
}, [onRenderStateChange]);
|
|
257
|
+
useEffect(() => stateStore.subscribe(() => {
|
|
258
|
+
const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
|
|
259
|
+
stateRef.current = snapshot;
|
|
260
|
+
onRenderStateChangeRef.current?.(snapshot);
|
|
261
|
+
}), [stateStore]);
|
|
226
262
|
useEffect(() => {
|
|
227
263
|
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
228
264
|
if (!catalogResult.success) {
|
|
@@ -242,10 +278,6 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
242
278
|
confetti: 0,
|
|
243
279
|
fireworks: 0,
|
|
244
280
|
});
|
|
245
|
-
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
246
|
-
useEffect(() => {
|
|
247
|
-
onRenderStateChangeRef.current = onRenderStateChange;
|
|
248
|
-
}, [onRenderStateChange]);
|
|
249
281
|
useEffect(() => {
|
|
250
282
|
const effectsToPresent = getUnpresentedSnapEffects(stateRef.current, snapEffects);
|
|
251
283
|
if (effectsToPresent.length === 0) {
|
|
@@ -262,7 +294,10 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
262
294
|
return;
|
|
263
295
|
}
|
|
264
296
|
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
265
|
-
|
|
297
|
+
const meta = recordValue(stateRef.current.__snapRender);
|
|
298
|
+
stateStore.update({
|
|
299
|
+
"/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
|
|
300
|
+
});
|
|
266
301
|
}
|
|
267
302
|
setEffectRunKeys((current) => ({
|
|
268
303
|
confetti: effectsToPresent.includes("confetti")
|
|
@@ -276,7 +311,7 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
276
311
|
? current.fireworks
|
|
277
312
|
: 0,
|
|
278
313
|
}));
|
|
279
|
-
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
314
|
+
}, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
|
|
280
315
|
const accentName = snap.theme?.accent ?? "purple";
|
|
281
316
|
const accentHex = useMemo(() => resolveSnapPaletteHex(accentName, appearance), [accentName, appearance]);
|
|
282
317
|
const previewSurfaceStyle = useMemo(() => {
|
|
@@ -288,33 +323,52 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
288
323
|
...vars,
|
|
289
324
|
};
|
|
290
325
|
}, [accentName, appearance]);
|
|
326
|
+
const applyActionActivityState = useCallback((name, params, pending) => {
|
|
327
|
+
stateStore.update(Object.fromEntries(buildActionActivityStateChanges({
|
|
328
|
+
actionName: name,
|
|
329
|
+
params,
|
|
330
|
+
pending,
|
|
331
|
+
}).map(({ path, value }) => [path, value])));
|
|
332
|
+
}, [stateStore]);
|
|
333
|
+
const setActionPending = useCallback((name, params) => {
|
|
334
|
+
pendingActionCountRef.current += 1;
|
|
335
|
+
applyActionActivityState(name, params, true);
|
|
336
|
+
}, [applyActionActivityState]);
|
|
337
|
+
const setActionSettled = useCallback((name, params) => {
|
|
338
|
+
pendingActionCountRef.current = Math.max(0, pendingActionCountRef.current - 1);
|
|
339
|
+
applyActionActivityState(name, params, false);
|
|
340
|
+
}, [applyActionActivityState]);
|
|
291
341
|
const handleAction = useCallback((name, params) => {
|
|
292
342
|
const inputs = (stateRef.current.inputs ?? {});
|
|
293
343
|
const p = (params ?? {});
|
|
344
|
+
let result;
|
|
345
|
+
setActionPending(name, p);
|
|
294
346
|
switch (name) {
|
|
295
347
|
case "submit":
|
|
296
|
-
handlers.submit(String(p.target ?? ""), inputs);
|
|
348
|
+
result = handlers.submit(String(p.target ?? ""), inputs);
|
|
297
349
|
break;
|
|
298
350
|
case "open_url":
|
|
299
|
-
handlers.open_url(String(p.target ?? ""));
|
|
351
|
+
result = handlers.open_url(String(p.target ?? ""));
|
|
300
352
|
break;
|
|
301
353
|
case "open_snap":
|
|
302
|
-
handlers.open_snap(String(p.target ?? ""));
|
|
354
|
+
result = handlers.open_snap(String(p.target ?? ""));
|
|
303
355
|
break;
|
|
304
356
|
case "open_mini_app":
|
|
305
|
-
handlers.open_mini_app(String(p.target ?? ""));
|
|
357
|
+
result = handlers.open_mini_app(String(p.target ?? ""));
|
|
306
358
|
break;
|
|
307
359
|
case "view_cast":
|
|
308
|
-
handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
360
|
+
result = handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
309
361
|
break;
|
|
310
362
|
case "view_profile":
|
|
311
|
-
handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
363
|
+
result = handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
312
364
|
break;
|
|
313
365
|
case "view_channel":
|
|
314
|
-
handlers.view_channel({
|
|
366
|
+
result = handlers.view_channel({
|
|
367
|
+
channelKey: String(p.channelKey ?? ""),
|
|
368
|
+
});
|
|
315
369
|
break;
|
|
316
370
|
case "compose_cast":
|
|
317
|
-
handlers.compose_cast({
|
|
371
|
+
result = handlers.compose_cast({
|
|
318
372
|
text: p.text ? String(p.text) : undefined,
|
|
319
373
|
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
320
374
|
embeds: Array.isArray(p.embeds)
|
|
@@ -323,10 +377,10 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
323
377
|
});
|
|
324
378
|
break;
|
|
325
379
|
case "view_token":
|
|
326
|
-
handlers.view_token({ token: String(p.token ?? "") });
|
|
380
|
+
result = handlers.view_token({ token: String(p.token ?? "") });
|
|
327
381
|
break;
|
|
328
382
|
case "send_token":
|
|
329
|
-
handlers.send_token({
|
|
383
|
+
result = handlers.send_token({
|
|
330
384
|
token: String(p.token ?? ""),
|
|
331
385
|
amount: p.amount ? String(p.amount) : undefined,
|
|
332
386
|
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
@@ -336,13 +390,13 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
336
390
|
});
|
|
337
391
|
break;
|
|
338
392
|
case "swap_token":
|
|
339
|
-
handlers.swap_token({
|
|
393
|
+
result = handlers.swap_token({
|
|
340
394
|
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
341
395
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
342
396
|
});
|
|
343
397
|
break;
|
|
344
398
|
case "send_transaction":
|
|
345
|
-
handlers.send_transaction?.({
|
|
399
|
+
result = handlers.send_transaction?.({
|
|
346
400
|
chainId: String(p.chainId ?? ""),
|
|
347
401
|
to: String(p.to ?? ""),
|
|
348
402
|
data: optionalString(p.data),
|
|
@@ -356,9 +410,20 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
|
|
|
356
410
|
default:
|
|
357
411
|
break;
|
|
358
412
|
}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
413
|
+
if (result instanceof Promise) {
|
|
414
|
+
void result.finally(() => {
|
|
415
|
+
setActionSettled(name, p);
|
|
416
|
+
}).catch(() => { });
|
|
417
|
+
}
|
|
418
|
+
else {
|
|
419
|
+
setActionSettled(name, p);
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}, [handlers, setActionPending, setActionSettled]);
|
|
423
|
+
return (_jsxs("div", { "data-snap-view-root": true, style: { position: "relative", width: "100%" }, onClickCapture: (event) => {
|
|
424
|
+
if (!hasPendingSnapAction(stateRef.current))
|
|
425
|
+
return;
|
|
426
|
+
event.preventDefault();
|
|
427
|
+
event.stopPropagation();
|
|
428
|
+
}, 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, store: stateStore, loading: false, onAction: handleAction, children: loadingOverlay === undefined ? (_jsx(SnapPendingActionOverlay, { appearance: appearance, accentHex: accentHex })) : null }, pageKey) }) }) })] }));
|
|
364
429
|
}
|
|
@@ -49,7 +49,7 @@ export function SnapCardV1({ snap, handlers, loading = false, appearance = "dark
|
|
|
49
49
|
position: "relative",
|
|
50
50
|
width: "100%",
|
|
51
51
|
maxWidth,
|
|
52
|
-
}, children: [_jsxs("div", { style: {
|
|
52
|
+
}, children: [_jsxs("div", { "data-snap-card-surface": true, style: {
|
|
53
53
|
position: "relative",
|
|
54
54
|
overflow: "hidden",
|
|
55
55
|
...(plain ? {} : {
|
|
@@ -90,7 +90,7 @@ export function SnapCardV2({ snap, handlers, loading = false, appearance = "dark
|
|
|
90
90
|
position: "relative",
|
|
91
91
|
width: "100%",
|
|
92
92
|
maxWidth,
|
|
93
|
-
}, children: [_jsxs("div", { style: {
|
|
93
|
+
}, children: [_jsxs("div", { "data-snap-card-surface": true, style: {
|
|
94
94
|
position: "relative",
|
|
95
95
|
maxHeight: containerMaxHeight,
|
|
96
96
|
overflow: "hidden",
|
|
@@ -22,6 +22,7 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
22
22
|
const label = String(props.label ?? "Action");
|
|
23
23
|
const variant = String(props.variant ?? "secondary");
|
|
24
24
|
const isPrimary = variant === "primary";
|
|
25
|
+
const disabled = props.disabled === true;
|
|
25
26
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
26
27
|
const textColor = isPrimary ? "#fff" : colors.text;
|
|
27
28
|
const iconColor = isPrimary ? "#fff" : colors.text;
|
|
@@ -37,7 +38,10 @@ export function SnapActionButton({ element, emit, }) {
|
|
|
37
38
|
? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
|
|
38
39
|
: { backgroundColor: pressed ? colors.mutedHover : colors.muted },
|
|
39
40
|
pressed && styles.pressed,
|
|
40
|
-
|
|
41
|
+
disabled && styles.disabled,
|
|
42
|
+
], disabled: disabled, onPress: () => {
|
|
43
|
+
if (disabled)
|
|
44
|
+
return;
|
|
41
45
|
if (runPaginatorAction(stateStore, paginatorAction))
|
|
42
46
|
return;
|
|
43
47
|
void (async () => {
|
|
@@ -85,4 +89,5 @@ const styles = StyleSheet.create({
|
|
|
85
89
|
paddingVertical: 6,
|
|
86
90
|
},
|
|
87
91
|
pressed: { opacity: 0.88 },
|
|
92
|
+
disabled: { opacity: 0.62 },
|
|
88
93
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createStateStore } from "@json-render/react-native";
|
|
2
3
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
4
|
import { SnapCatalogView } from "./catalog-renderer.js";
|
|
4
5
|
import { ConfettiOverlay } from "./confetti-overlay.js";
|
|
@@ -8,7 +9,7 @@ import { SnapVersionProvider } from "./snap-version-context.js";
|
|
|
8
9
|
import { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
9
10
|
import { ActivityIndicator, StyleSheet, View } from "react-native";
|
|
10
11
|
import { DEFAULT_THEME_ACCENT, PALETTE_LIGHT_HEX, PALETTE_DARK_HEX, } from "@farcaster/snap";
|
|
11
|
-
import {
|
|
12
|
+
import { buildActionActivityStateChanges, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, hasPendingSnapAction, markSnapEffectsPresented, } from "../render-state.js";
|
|
12
13
|
function asRecord(value) {
|
|
13
14
|
return value && typeof value === "object"
|
|
14
15
|
? value
|
|
@@ -17,6 +18,11 @@ function asRecord(value) {
|
|
|
17
18
|
function optionalString(value) {
|
|
18
19
|
return value ? String(value) : undefined;
|
|
19
20
|
}
|
|
21
|
+
function recordValue(value) {
|
|
22
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
23
|
+
? value
|
|
24
|
+
: undefined;
|
|
25
|
+
}
|
|
20
26
|
function withDefaultElementProps(spec) {
|
|
21
27
|
if (!spec || typeof spec !== "object" || !("elements" in spec))
|
|
22
28
|
return spec;
|
|
@@ -54,10 +60,26 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
54
60
|
initialRenderState,
|
|
55
61
|
themeAccent: snap.theme?.accent,
|
|
56
62
|
}), [initialRenderState, spec.state, snap.theme?.accent]);
|
|
63
|
+
const stateStore = useMemo(() => createStateStore(initialState), [
|
|
64
|
+
initialState,
|
|
65
|
+
]);
|
|
57
66
|
const stateRef = useRef(initialState);
|
|
67
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
68
|
+
const pendingActionCountRef = useRef(0);
|
|
69
|
+
const [hasPendingAction, setHasPendingAction] = useState(false);
|
|
70
|
+
const [actionActivityVersion, setActionActivityVersion] = useState(0);
|
|
58
71
|
useEffect(() => {
|
|
59
72
|
stateRef.current = cloneSnapRenderState(initialState);
|
|
60
73
|
}, [initialState]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
76
|
+
}, [onRenderStateChange]);
|
|
77
|
+
useEffect(() => stateStore.subscribe(() => {
|
|
78
|
+
const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
|
|
79
|
+
stateRef.current = snapshot;
|
|
80
|
+
setHasPendingAction(hasPendingSnapAction(snapshot));
|
|
81
|
+
onRenderStateChangeRef.current?.(snapshot);
|
|
82
|
+
}), [stateStore]);
|
|
61
83
|
useEffect(() => {
|
|
62
84
|
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
63
85
|
if (!catalogResult.success) {
|
|
@@ -77,10 +99,6 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
77
99
|
confetti: 0,
|
|
78
100
|
fireworks: 0,
|
|
79
101
|
});
|
|
80
|
-
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
81
|
-
useEffect(() => {
|
|
82
|
-
onRenderStateChangeRef.current = onRenderStateChange;
|
|
83
|
-
}, [onRenderStateChange]);
|
|
84
102
|
useEffect(() => {
|
|
85
103
|
const effectsToPresent = getUnpresentedSnapEffects(stateRef.current, snapEffects);
|
|
86
104
|
if (effectsToPresent.length === 0) {
|
|
@@ -97,7 +115,10 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
97
115
|
return;
|
|
98
116
|
}
|
|
99
117
|
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
100
|
-
|
|
118
|
+
const meta = recordValue(stateRef.current.__snapRender);
|
|
119
|
+
stateStore.update({
|
|
120
|
+
"/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
|
|
121
|
+
});
|
|
101
122
|
}
|
|
102
123
|
setEffectRunKeys((current) => ({
|
|
103
124
|
confetti: effectsToPresent.includes("confetti")
|
|
@@ -111,47 +132,70 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
111
132
|
? current.fireworks
|
|
112
133
|
: 0,
|
|
113
134
|
}));
|
|
114
|
-
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
135
|
+
}, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
|
|
115
136
|
const handlersRef = useRef(handlers);
|
|
116
137
|
handlersRef.current = handlers;
|
|
138
|
+
const applyActionActivityState = useCallback((name, params, pending) => {
|
|
139
|
+
stateStore.update(Object.fromEntries(buildActionActivityStateChanges({
|
|
140
|
+
actionName: name,
|
|
141
|
+
params,
|
|
142
|
+
pending,
|
|
143
|
+
}).map(({ path, value }) => [path, value])));
|
|
144
|
+
}, [stateStore]);
|
|
145
|
+
const setActionPending = useCallback((name, params) => {
|
|
146
|
+
pendingActionCountRef.current += 1;
|
|
147
|
+
setHasPendingAction(true);
|
|
148
|
+
setActionActivityVersion((version) => version + 1);
|
|
149
|
+
applyActionActivityState(name, params, true);
|
|
150
|
+
}, [applyActionActivityState]);
|
|
151
|
+
const setActionSettled = useCallback((name, params) => {
|
|
152
|
+
pendingActionCountRef.current = Math.max(0, pendingActionCountRef.current - 1);
|
|
153
|
+
applyActionActivityState(name, params, false);
|
|
154
|
+
if (pendingActionCountRef.current === 0) {
|
|
155
|
+
setHasPendingAction(false);
|
|
156
|
+
}
|
|
157
|
+
setActionActivityVersion((version) => version + 1);
|
|
158
|
+
}, [applyActionActivityState]);
|
|
117
159
|
const handleAction = useCallback((name, params) => {
|
|
118
160
|
const inputs = (stateRef.current.inputs ?? {});
|
|
119
161
|
const p = (params ?? {});
|
|
120
162
|
const h = handlersRef.current;
|
|
163
|
+
let result;
|
|
164
|
+
setActionPending(name, p);
|
|
121
165
|
switch (name) {
|
|
122
166
|
case "submit":
|
|
123
|
-
h.submit(String(p.target ?? ""), inputs);
|
|
167
|
+
result = h.submit(String(p.target ?? ""), inputs);
|
|
124
168
|
break;
|
|
125
169
|
case "open_url":
|
|
126
|
-
h.open_url(String(p.target ?? ""));
|
|
170
|
+
result = h.open_url(String(p.target ?? ""));
|
|
127
171
|
break;
|
|
128
172
|
case "open_snap":
|
|
129
|
-
h.open_snap(String(p.target ?? ""));
|
|
173
|
+
result = h.open_snap(String(p.target ?? ""));
|
|
130
174
|
break;
|
|
131
175
|
case "open_mini_app":
|
|
132
|
-
h.open_mini_app(String(p.target ?? ""));
|
|
176
|
+
result = h.open_mini_app(String(p.target ?? ""));
|
|
133
177
|
break;
|
|
134
178
|
case "view_cast":
|
|
135
|
-
h.view_cast({ hash: String(p.hash ?? "") });
|
|
179
|
+
result = h.view_cast({ hash: String(p.hash ?? "") });
|
|
136
180
|
break;
|
|
137
181
|
case "view_profile":
|
|
138
|
-
h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
182
|
+
result = h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
139
183
|
break;
|
|
140
184
|
case "view_channel":
|
|
141
|
-
h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
185
|
+
result = h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
142
186
|
break;
|
|
143
187
|
case "compose_cast":
|
|
144
|
-
h.compose_cast({
|
|
188
|
+
result = h.compose_cast({
|
|
145
189
|
text: p.text ? String(p.text) : undefined,
|
|
146
190
|
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
147
191
|
embeds: Array.isArray(p.embeds) ? p.embeds : undefined,
|
|
148
192
|
});
|
|
149
193
|
break;
|
|
150
194
|
case "view_token":
|
|
151
|
-
h.view_token({ token: String(p.token ?? "") });
|
|
195
|
+
result = h.view_token({ token: String(p.token ?? "") });
|
|
152
196
|
break;
|
|
153
197
|
case "send_token":
|
|
154
|
-
h.send_token({
|
|
198
|
+
result = h.send_token({
|
|
155
199
|
token: String(p.token ?? ""),
|
|
156
200
|
amount: p.amount ? String(p.amount) : undefined,
|
|
157
201
|
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
@@ -161,13 +205,13 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
161
205
|
});
|
|
162
206
|
break;
|
|
163
207
|
case "swap_token":
|
|
164
|
-
h.swap_token({
|
|
208
|
+
result = h.swap_token({
|
|
165
209
|
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
166
210
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
167
211
|
});
|
|
168
212
|
break;
|
|
169
213
|
case "send_transaction":
|
|
170
|
-
h.send_transaction?.({
|
|
214
|
+
result = h.send_transaction?.({
|
|
171
215
|
chainId: String(p.chainId ?? ""),
|
|
172
216
|
to: String(p.to ?? ""),
|
|
173
217
|
data: optionalString(p.data),
|
|
@@ -181,11 +225,20 @@ export function SnapViewCoreInner({ snap, handlers, loading = false, loadingOver
|
|
|
181
225
|
default:
|
|
182
226
|
break;
|
|
183
227
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
228
|
+
if (result instanceof Promise) {
|
|
229
|
+
void result.finally(() => {
|
|
230
|
+
setActionSettled(name, p);
|
|
231
|
+
}).catch(() => { });
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
setActionSettled(name, p);
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}, [setActionPending, setActionSettled]);
|
|
238
|
+
const showLoadingOverlay = loading ||
|
|
239
|
+
hasPendingAction ||
|
|
240
|
+
(actionActivityVersion >= 0 && pendingActionCountRef.current > 0);
|
|
241
|
+
return (_jsxs(View, { style: styles.container, onStartShouldSetResponderCapture: () => hasPendingSnapAction(stateRef.current), children: [showLoadingOverlay ? (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, store: stateStore, loading: false, onAction: handleAction }, pageKey) }), showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks))] }));
|
|
189
242
|
}
|
|
190
243
|
export function SnapLoadingOverlay({ appearance, accentHex, }) {
|
|
191
244
|
return (_jsx(View, { style: [
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
2
|
import type { SnapRenderState } from "../render-state.js";
|
|
3
|
+
import type { SnapTransactionResult } from "../schemas.js";
|
|
3
4
|
export type { SnapRenderState };
|
|
4
5
|
export type JsonValue = string | number | boolean | null | JsonValue[] | {
|
|
5
6
|
[key: string]: JsonValue;
|
|
@@ -54,5 +55,5 @@ export type SnapActionHandlers = {
|
|
|
54
55
|
sellToken?: string;
|
|
55
56
|
buyToken?: string;
|
|
56
57
|
}) => void;
|
|
57
|
-
send_transaction?: (params: SnapSendTransactionParams) => void
|
|
58
|
+
send_transaction?: (params: SnapSendTransactionParams) => void | Promise<void | SnapTransactionResult>;
|
|
58
59
|
};
|