@farcaster/snap 2.8.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/react/catalog-renderer.d.ts +5 -5
  4. package/dist/react/catalog-renderer.js +16 -4
  5. package/dist/react/components/action-button.js +23 -5
  6. package/dist/react/index.d.ts +2 -13
  7. package/dist/react/snap-view-core.js +90 -45
  8. package/dist/react/v1/snap-view.js +1 -1
  9. package/dist/react/v2/snap-view.js +1 -1
  10. package/dist/react-native/components/snap-action-button.js +6 -1
  11. package/dist/react-native/snap-view-core.js +77 -44
  12. package/dist/react-native/types.d.ts +2 -13
  13. package/dist/render-state.d.ts +9 -0
  14. package/dist/render-state.js +27 -0
  15. package/dist/schemas.d.ts +123 -3
  16. package/dist/schemas.js +53 -2
  17. package/dist/server/parseRequest.js +19 -3
  18. package/dist/ui/button.d.ts +1 -0
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/catalog.d.ts +13 -14
  21. package/dist/ui/catalog.js +15 -22
  22. package/package.json +1 -1
  23. package/src/index.ts +7 -0
  24. package/src/react/catalog-renderer.tsx +57 -3
  25. package/src/react/components/action-button.tsx +32 -3
  26. package/src/react/index.tsx +4 -14
  27. package/src/react/snap-view-core.tsx +144 -48
  28. package/src/react/v1/snap-view.tsx +1 -0
  29. package/src/react/v2/snap-view.tsx +1 -0
  30. package/src/react-native/components/snap-action-button.tsx +6 -1
  31. package/src/react-native/snap-view-core.tsx +114 -48
  32. package/src/react-native/types.ts +4 -14
  33. package/src/render-state.ts +46 -0
  34. package/src/schemas.ts +73 -2
  35. package/src/server/parseRequest.ts +37 -6
  36. package/src/ui/button.ts +1 -0
  37. package/src/ui/catalog.ts +16 -25
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
- * Maps snap json-render catalog types to React components.
3
- * Keys match the snap wire-format `type` strings exactly.
4
- */
5
- export declare const SnapCatalogView: import("react").ComponentType<import("@json-render/react").CreateRendererProps>;
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 { createRenderer } from "@json-render/react";
3
- import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
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
- export const SnapCatalogView = createRenderer(snapJsonRenderCatalog, {
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
  }
@@ -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
  };
@@ -23,17 +24,6 @@ export type SnapSendTransactionParams = {
23
24
  maxFeePerGas?: string;
24
25
  maxPriorityFeePerGas?: string;
25
26
  };
26
- export type SnapSendCallsParams = {
27
- version?: "1.0";
28
- chainId: string;
29
- atomicRequired?: boolean;
30
- id?: string;
31
- calls: Array<{
32
- to?: string;
33
- data?: string;
34
- value?: string;
35
- }>;
36
- };
37
27
  export type SnapActionHandlers = {
38
28
  submit: (target: string, inputs: Record<string, JsonValue>) => void;
39
29
  open_url: (target: string) => void;
@@ -66,8 +56,7 @@ export type SnapActionHandlers = {
66
56
  sellToken?: string;
67
57
  buyToken?: string;
68
58
  }) => void;
69
- send_transaction?: (params: SnapSendTransactionParams) => void;
70
- send_calls?: (params: SnapSendCallsParams) => void;
59
+ send_transaction?: (params: SnapSendTransactionParams) => void | Promise<void | SnapTransactionResult>;
71
60
  };
72
61
  export type { SnapRenderState };
73
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 { applyStatePaths, buildInitialRenderState, cloneSnapRenderState, getUnpresentedSnapEffects, markSnapEffectsPresented, } from "../render-state.js";
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
- onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
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({ channelKey: String(p.channelKey ?? "") });
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),
@@ -353,32 +407,23 @@ export function SnapViewCore({ snap, handlers, loading = false, appearance = "da
353
407
  maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
354
408
  });
355
409
  break;
356
- case "send_calls":
357
- handlers.send_calls?.({
358
- version: p.version === "1.0" ? "1.0" : undefined,
359
- chainId: String(p.chainId ?? ""),
360
- atomicRequired: typeof p.atomicRequired === "boolean"
361
- ? p.atomicRequired
362
- : undefined,
363
- id: optionalString(p.id),
364
- calls: Array.isArray(p.calls)
365
- ? p.calls.map((call) => {
366
- const c = asRecord(call);
367
- return {
368
- to: optionalString(c.to),
369
- data: optionalString(c.data),
370
- value: optionalString(c.value),
371
- };
372
- })
373
- : [],
374
- });
375
- break;
376
410
  default:
377
411
  break;
378
412
  }
379
- }, [handlers]);
380
- return (_jsxs("div", { style: { position: "relative", width: "100%" }, children: [showConfetti && effectRunKeys.confetti > 0 && (_jsx(ConfettiOverlay, {}, effectRunKeys.confetti)), showFireworks && effectRunKeys.fireworks > 0 && (_jsx(FireworksOverlay, {}, effectRunKeys.fireworks)), loadingOverlay === undefined ? (_jsx(SnapLoadingOverlay, { appearance: appearance, accentHex: accentHex, active: loading })) : loading ? (_jsx(_Fragment, { children: loadingOverlay })) : null, _jsx("div", { style: previewSurfaceStyle, children: _jsx(SnapPreviewAccentProvider, { pageAccent: snap.theme?.accent, appearance: appearance, children: _jsx(SnapVersionProvider, { value: snap.version === "2.0" ? "2.0" : "1.0", children: _jsx(SnapCatalogView, { spec: spec, state: initialState, loading: false, onStateChange: (changes) => {
381
- applyStatePaths(stateRef.current, changes);
382
- onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
383
- }, onAction: handleAction }, pageKey) }) }) })] }));
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) }) }) })] }));
384
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
- ], onPress: () => {
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
  });