@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
package/src/react/index.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { Spec } from "@json-render/core";
|
|
|
4
4
|
import type { ReactNode } from "react";
|
|
5
5
|
import type { ValidationResult } from "../validator.js";
|
|
6
6
|
import { SPEC_VERSION_2 } from "../constants";
|
|
7
|
+
import type { SnapRenderState } from "../render-state";
|
|
7
8
|
import { SnapCardV1 } from "./v1/snap-view";
|
|
8
9
|
import { SnapCardV2 } from "./v2/snap-view";
|
|
9
10
|
|
|
@@ -24,6 +25,29 @@ export type SnapPage = {
|
|
|
24
25
|
ui: Spec;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
export type SnapSendTransactionParams = {
|
|
29
|
+
chainId: string;
|
|
30
|
+
to: string;
|
|
31
|
+
data?: string;
|
|
32
|
+
value?: string;
|
|
33
|
+
gas?: string;
|
|
34
|
+
gasPrice?: string;
|
|
35
|
+
maxFeePerGas?: string;
|
|
36
|
+
maxPriorityFeePerGas?: string;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export type SnapSendCallsParams = {
|
|
40
|
+
version?: "1.0";
|
|
41
|
+
chainId: string;
|
|
42
|
+
atomicRequired?: boolean;
|
|
43
|
+
id?: string;
|
|
44
|
+
calls: Array<{
|
|
45
|
+
to?: string;
|
|
46
|
+
data?: string;
|
|
47
|
+
value?: string;
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
50
|
+
|
|
27
51
|
export type SnapActionHandlers = {
|
|
28
52
|
submit: (target: string, inputs: Record<string, JsonValue>) => void;
|
|
29
53
|
open_url: (target: string) => void;
|
|
@@ -44,8 +68,12 @@ export type SnapActionHandlers = {
|
|
|
44
68
|
recipientAddress?: string;
|
|
45
69
|
}) => void;
|
|
46
70
|
swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
|
|
71
|
+
send_transaction?: (params: SnapSendTransactionParams) => void;
|
|
72
|
+
send_calls?: (params: SnapSendCallsParams) => void;
|
|
47
73
|
};
|
|
48
74
|
|
|
75
|
+
export type { SnapRenderState };
|
|
76
|
+
|
|
49
77
|
// ─── SnapCard ────────────────────────────────────────
|
|
50
78
|
|
|
51
79
|
export function SnapCard({
|
|
@@ -60,6 +88,8 @@ export function SnapCard({
|
|
|
60
88
|
actionError,
|
|
61
89
|
plain = false,
|
|
62
90
|
loadingOverlay,
|
|
91
|
+
initialRenderState,
|
|
92
|
+
onRenderStateChange,
|
|
63
93
|
}: {
|
|
64
94
|
snap: SnapPage;
|
|
65
95
|
handlers: SnapActionHandlers;
|
|
@@ -76,6 +106,10 @@ export function SnapCard({
|
|
|
76
106
|
plain?: boolean;
|
|
77
107
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
78
108
|
loadingOverlay?: ReactNode;
|
|
109
|
+
/** JSON-render local state used to seed this presenter mount. */
|
|
110
|
+
initialRenderState?: SnapRenderState;
|
|
111
|
+
/** Called with the full JSON-render local state after state changes. */
|
|
112
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
79
113
|
}) {
|
|
80
114
|
if (snap.version === SPEC_VERSION_2) {
|
|
81
115
|
return (
|
|
@@ -91,6 +125,8 @@ export function SnapCard({
|
|
|
91
125
|
actionError={actionError}
|
|
92
126
|
plain={plain}
|
|
93
127
|
loadingOverlay={loadingOverlay}
|
|
128
|
+
initialRenderState={initialRenderState}
|
|
129
|
+
onRenderStateChange={onRenderStateChange}
|
|
94
130
|
/>
|
|
95
131
|
);
|
|
96
132
|
}
|
|
@@ -105,6 +141,8 @@ export function SnapCard({
|
|
|
105
141
|
actionError={actionError}
|
|
106
142
|
plain={plain}
|
|
107
143
|
loadingOverlay={loadingOverlay}
|
|
144
|
+
initialRenderState={initialRenderState}
|
|
145
|
+
onRenderStateChange={onRenderStateChange}
|
|
108
146
|
/>
|
|
109
147
|
);
|
|
110
148
|
}
|
|
@@ -7,6 +7,14 @@ import { SnapPreviewAccentProvider } from "./accent-context";
|
|
|
7
7
|
import { SnapVersionProvider } from "./snap-version-context";
|
|
8
8
|
import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
|
|
9
9
|
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
|
|
10
|
+
import {
|
|
11
|
+
applyStatePaths,
|
|
12
|
+
buildInitialRenderState,
|
|
13
|
+
cloneSnapRenderState,
|
|
14
|
+
getUnpresentedSnapEffects,
|
|
15
|
+
markSnapEffectsPresented,
|
|
16
|
+
type SnapRenderState,
|
|
17
|
+
} from "../render-state";
|
|
10
18
|
import {
|
|
11
19
|
type CSSProperties,
|
|
12
20
|
type ReactNode,
|
|
@@ -18,45 +26,14 @@ import {
|
|
|
18
26
|
} from "react";
|
|
19
27
|
import type { JsonValue, SnapActionHandlers, SnapPage } from "./index";
|
|
20
28
|
|
|
21
|
-
|
|
29
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
30
|
+
return value && typeof value === "object"
|
|
31
|
+
? (value as Record<string, unknown>)
|
|
32
|
+
: {};
|
|
33
|
+
}
|
|
22
34
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
changes:
|
|
26
|
-
| { path: string; value: unknown }[]
|
|
27
|
-
| Record<string, unknown>
|
|
28
|
-
| null
|
|
29
|
-
| undefined,
|
|
30
|
-
): void {
|
|
31
|
-
if (!changes) return;
|
|
32
|
-
const entries = Array.isArray(changes)
|
|
33
|
-
? changes.map((c) => [c.path, c.value] as const)
|
|
34
|
-
: Object.entries(changes);
|
|
35
|
-
for (const [path, value] of entries) {
|
|
36
|
-
const trimmed = path.startsWith("/") ? path : `/${path}`;
|
|
37
|
-
const parts = trimmed.split("/").filter(Boolean);
|
|
38
|
-
if (parts.length < 2) continue;
|
|
39
|
-
const [top, ...rest] = parts;
|
|
40
|
-
if (top === "inputs") {
|
|
41
|
-
if (typeof model.inputs !== "object" || model.inputs === null) {
|
|
42
|
-
model.inputs = {};
|
|
43
|
-
}
|
|
44
|
-
const inputs = model.inputs as Record<string, unknown>;
|
|
45
|
-
if (rest.length === 1) {
|
|
46
|
-
inputs[rest[0]!] = value;
|
|
47
|
-
}
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (top === "theme") {
|
|
51
|
-
if (typeof model.theme !== "object" || model.theme === null) {
|
|
52
|
-
model.theme = {};
|
|
53
|
-
}
|
|
54
|
-
const theme = model.theme as Record<string, unknown>;
|
|
55
|
-
if (rest.length === 1) {
|
|
56
|
-
theme[rest[0]!] = value;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
35
|
+
function optionalString(value: unknown): string | undefined {
|
|
36
|
+
return value ? String(value) : undefined;
|
|
60
37
|
}
|
|
61
38
|
|
|
62
39
|
function withDefaultElementProps(spec: Spec): Spec {
|
|
@@ -137,7 +114,18 @@ function ConfettiOverlay() {
|
|
|
137
114
|
}}
|
|
138
115
|
>
|
|
139
116
|
{pieces.map(
|
|
140
|
-
({
|
|
117
|
+
({
|
|
118
|
+
id,
|
|
119
|
+
left,
|
|
120
|
+
delay,
|
|
121
|
+
duration,
|
|
122
|
+
color,
|
|
123
|
+
size,
|
|
124
|
+
rotation,
|
|
125
|
+
isCircle,
|
|
126
|
+
driftX,
|
|
127
|
+
driftMid,
|
|
128
|
+
}) => (
|
|
141
129
|
<div
|
|
142
130
|
key={id}
|
|
143
131
|
style={
|
|
@@ -179,8 +167,7 @@ function FireworksOverlay() {
|
|
|
179
167
|
y: 10 + Math.random() * 50,
|
|
180
168
|
delay: b * 0.5 + Math.random() * 0.2,
|
|
181
169
|
particles: Array.from({ length: 24 }, (_, p) => {
|
|
182
|
-
const angle =
|
|
183
|
-
(p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
170
|
+
const angle = (p / 24) * Math.PI * 2 + (Math.random() - 0.5) * 0.2;
|
|
184
171
|
const dist = 55 + Math.random() * 60;
|
|
185
172
|
return {
|
|
186
173
|
id: p,
|
|
@@ -288,9 +275,7 @@ export function SnapLoadingOverlay({
|
|
|
288
275
|
zIndex: 10,
|
|
289
276
|
background: tint,
|
|
290
277
|
backdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
|
|
291
|
-
WebkitBackdropFilter: active
|
|
292
|
-
? "blur(10px) saturate(1.05)"
|
|
293
|
-
: "none",
|
|
278
|
+
WebkitBackdropFilter: active ? "blur(10px) saturate(1.05)" : "none",
|
|
294
279
|
opacity: active ? 1 : 0,
|
|
295
280
|
pointerEvents: active ? "auto" : "none",
|
|
296
281
|
transition: "opacity 0.28s ease, backdrop-filter 0.28s ease",
|
|
@@ -349,6 +334,8 @@ export function SnapViewCore({
|
|
|
349
334
|
loading = false,
|
|
350
335
|
appearance = "dark",
|
|
351
336
|
loadingOverlay,
|
|
337
|
+
initialRenderState,
|
|
338
|
+
onRenderStateChange,
|
|
352
339
|
}: {
|
|
353
340
|
snap: SnapPage;
|
|
354
341
|
handlers: SnapActionHandlers;
|
|
@@ -359,21 +346,24 @@ export function SnapViewCore({
|
|
|
359
346
|
* the built-in spinner + backdrop is used. Pass `null` to render nothing.
|
|
360
347
|
*/
|
|
361
348
|
loadingOverlay?: ReactNode;
|
|
349
|
+
initialRenderState?: SnapRenderState;
|
|
350
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
362
351
|
}) {
|
|
363
352
|
const spec = useMemo(() => withDefaultElementProps(snap.ui), [snap.ui]);
|
|
364
|
-
const initialState = useMemo(
|
|
353
|
+
const initialState = useMemo(
|
|
354
|
+
() =>
|
|
355
|
+
buildInitialRenderState({
|
|
356
|
+
specState: spec.state,
|
|
357
|
+
initialRenderState,
|
|
358
|
+
themeAccent: snap.theme?.accent,
|
|
359
|
+
}),
|
|
360
|
+
[initialRenderState, spec.state, snap.theme?.accent],
|
|
361
|
+
);
|
|
365
362
|
|
|
366
363
|
const stateRef = useRef<Record<string, unknown>>(initialState);
|
|
367
364
|
|
|
368
365
|
useEffect(() => {
|
|
369
|
-
stateRef.current =
|
|
370
|
-
inputs: {
|
|
371
|
-
...((initialState.inputs ?? {}) as Record<string, unknown>),
|
|
372
|
-
},
|
|
373
|
-
theme: {
|
|
374
|
-
...((initialState.theme ?? {}) as Record<string, unknown>),
|
|
375
|
-
},
|
|
376
|
-
};
|
|
366
|
+
stateRef.current = cloneSnapRenderState(initialState);
|
|
377
367
|
}, [initialState]);
|
|
378
368
|
|
|
379
369
|
useEffect(() => {
|
|
@@ -389,14 +379,58 @@ export function SnapViewCore({
|
|
|
389
379
|
setPageKey((k) => k + 1);
|
|
390
380
|
}, [spec]);
|
|
391
381
|
|
|
392
|
-
const
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
382
|
+
const effectSignature = snap.effects?.join("\u0000") ?? "";
|
|
383
|
+
const snapEffects = useMemo(
|
|
384
|
+
() => (effectSignature ? effectSignature.split("\u0000") : []),
|
|
385
|
+
[effectSignature],
|
|
386
|
+
);
|
|
387
|
+
const showConfetti = snapEffects.includes("confetti");
|
|
388
|
+
const showFireworks = snapEffects.includes("fireworks");
|
|
389
|
+
const [effectRunKeys, setEffectRunKeys] = useState({
|
|
390
|
+
confetti: 0,
|
|
391
|
+
fireworks: 0,
|
|
392
|
+
});
|
|
393
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
394
|
+
useEffect(() => {
|
|
395
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
396
|
+
}, [onRenderStateChange]);
|
|
396
397
|
useEffect(() => {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
398
|
+
const effectsToPresent = getUnpresentedSnapEffects(
|
|
399
|
+
stateRef.current,
|
|
400
|
+
snapEffects,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
if (effectsToPresent.length === 0) {
|
|
404
|
+
setEffectRunKeys((current) => {
|
|
405
|
+
const next = {
|
|
406
|
+
confetti: showConfetti ? current.confetti : 0,
|
|
407
|
+
fireworks: showFireworks ? current.fireworks : 0,
|
|
408
|
+
};
|
|
409
|
+
return next.confetti === current.confetti &&
|
|
410
|
+
next.fireworks === current.fireworks
|
|
411
|
+
? current
|
|
412
|
+
: next;
|
|
413
|
+
});
|
|
414
|
+
return;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
418
|
+
onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
setEffectRunKeys((current) => ({
|
|
422
|
+
confetti: effectsToPresent.includes("confetti")
|
|
423
|
+
? current.confetti + 1
|
|
424
|
+
: showConfetti
|
|
425
|
+
? current.confetti
|
|
426
|
+
: 0,
|
|
427
|
+
fireworks: effectsToPresent.includes("fireworks")
|
|
428
|
+
? current.fireworks + 1
|
|
429
|
+
: showFireworks
|
|
430
|
+
? current.fireworks
|
|
431
|
+
: 0,
|
|
432
|
+
}));
|
|
433
|
+
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
400
434
|
|
|
401
435
|
const accentName = snap.theme?.accent ?? "purple";
|
|
402
436
|
|
|
@@ -469,6 +503,39 @@ export function SnapViewCore({
|
|
|
469
503
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
470
504
|
});
|
|
471
505
|
break;
|
|
506
|
+
case "send_transaction":
|
|
507
|
+
handlers.send_transaction?.({
|
|
508
|
+
chainId: String(p.chainId ?? ""),
|
|
509
|
+
to: String(p.to ?? ""),
|
|
510
|
+
data: optionalString(p.data),
|
|
511
|
+
value: optionalString(p.value),
|
|
512
|
+
gas: optionalString(p.gas),
|
|
513
|
+
gasPrice: optionalString(p.gasPrice),
|
|
514
|
+
maxFeePerGas: optionalString(p.maxFeePerGas),
|
|
515
|
+
maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
|
|
516
|
+
});
|
|
517
|
+
break;
|
|
518
|
+
case "send_calls":
|
|
519
|
+
handlers.send_calls?.({
|
|
520
|
+
version: p.version === "1.0" ? "1.0" : undefined,
|
|
521
|
+
chainId: String(p.chainId ?? ""),
|
|
522
|
+
atomicRequired:
|
|
523
|
+
typeof p.atomicRequired === "boolean"
|
|
524
|
+
? p.atomicRequired
|
|
525
|
+
: undefined,
|
|
526
|
+
id: optionalString(p.id),
|
|
527
|
+
calls: Array.isArray(p.calls)
|
|
528
|
+
? p.calls.map((call) => {
|
|
529
|
+
const c = asRecord(call);
|
|
530
|
+
return {
|
|
531
|
+
to: optionalString(c.to),
|
|
532
|
+
data: optionalString(c.data),
|
|
533
|
+
value: optionalString(c.value),
|
|
534
|
+
};
|
|
535
|
+
})
|
|
536
|
+
: [],
|
|
537
|
+
});
|
|
538
|
+
break;
|
|
472
539
|
default:
|
|
473
540
|
break;
|
|
474
541
|
}
|
|
@@ -478,8 +545,12 @@ export function SnapViewCore({
|
|
|
478
545
|
|
|
479
546
|
return (
|
|
480
547
|
<div style={{ position: "relative", width: "100%" }}>
|
|
481
|
-
{showConfetti &&
|
|
482
|
-
|
|
548
|
+
{showConfetti && effectRunKeys.confetti > 0 && (
|
|
549
|
+
<ConfettiOverlay key={effectRunKeys.confetti} />
|
|
550
|
+
)}
|
|
551
|
+
{showFireworks && effectRunKeys.fireworks > 0 && (
|
|
552
|
+
<FireworksOverlay key={effectRunKeys.fireworks} />
|
|
553
|
+
)}
|
|
483
554
|
{loadingOverlay === undefined ? (
|
|
484
555
|
<SnapLoadingOverlay
|
|
485
556
|
appearance={appearance}
|
|
@@ -503,6 +574,7 @@ export function SnapViewCore({
|
|
|
503
574
|
loading={false}
|
|
504
575
|
onStateChange={(changes) => {
|
|
505
576
|
applyStatePaths(stateRef.current, changes);
|
|
577
|
+
onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
|
|
506
578
|
}}
|
|
507
579
|
onAction={handleAction}
|
|
508
580
|
/>
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
|
|
5
5
|
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
6
|
-
import type { SnapPage,
|
|
6
|
+
import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../index";
|
|
7
7
|
|
|
8
8
|
const SNAP_MAX_HEIGHT = 500;
|
|
9
9
|
|
|
@@ -13,6 +13,8 @@ export function SnapViewV1({
|
|
|
13
13
|
loading = false,
|
|
14
14
|
appearance = "dark",
|
|
15
15
|
loadingOverlay,
|
|
16
|
+
initialRenderState,
|
|
17
|
+
onRenderStateChange,
|
|
16
18
|
}: {
|
|
17
19
|
snap: SnapPage;
|
|
18
20
|
handlers: SnapActionHandlers;
|
|
@@ -20,6 +22,8 @@ export function SnapViewV1({
|
|
|
20
22
|
appearance?: "light" | "dark";
|
|
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
|
}) {
|
|
24
28
|
return (
|
|
25
29
|
<SnapViewCore
|
|
@@ -28,6 +32,8 @@ export function SnapViewV1({
|
|
|
28
32
|
loading={loading}
|
|
29
33
|
appearance={appearance}
|
|
30
34
|
loadingOverlay={loadingOverlay}
|
|
35
|
+
initialRenderState={initialRenderState}
|
|
36
|
+
onRenderStateChange={onRenderStateChange}
|
|
31
37
|
/>
|
|
32
38
|
);
|
|
33
39
|
}
|
|
@@ -41,6 +47,8 @@ export function SnapCardV1({
|
|
|
41
47
|
actionError,
|
|
42
48
|
plain = false,
|
|
43
49
|
loadingOverlay,
|
|
50
|
+
initialRenderState,
|
|
51
|
+
onRenderStateChange,
|
|
44
52
|
}: {
|
|
45
53
|
snap: SnapPage;
|
|
46
54
|
handlers: SnapActionHandlers;
|
|
@@ -51,6 +59,10 @@ export function SnapCardV1({
|
|
|
51
59
|
plain?: boolean;
|
|
52
60
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
53
61
|
loadingOverlay?: ReactNode;
|
|
62
|
+
/** JSON-render local state used to seed this presenter mount. */
|
|
63
|
+
initialRenderState?: SnapRenderState;
|
|
64
|
+
/** Called with the full JSON-render local state after state changes. */
|
|
65
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
54
66
|
}) {
|
|
55
67
|
const isDark = appearance === "dark";
|
|
56
68
|
const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
|
|
@@ -135,6 +147,8 @@ export function SnapCardV1({
|
|
|
135
147
|
loading={loading}
|
|
136
148
|
appearance={appearance}
|
|
137
149
|
loadingOverlay={null}
|
|
150
|
+
initialRenderState={initialRenderState}
|
|
151
|
+
onRenderStateChange={onRenderStateChange}
|
|
138
152
|
/>
|
|
139
153
|
</div>
|
|
140
154
|
</div>
|
|
@@ -5,7 +5,7 @@ import { validateSnapResponse } from "../../validator.js";
|
|
|
5
5
|
import type { ValidationResult } from "../../validator.js";
|
|
6
6
|
import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
|
|
7
7
|
import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
|
|
8
|
-
import type { SnapPage,
|
|
8
|
+
import type { SnapActionHandlers, SnapPage, SnapRenderState } from "../index";
|
|
9
9
|
|
|
10
10
|
const SNAP_MAX_HEIGHT = 500;
|
|
11
11
|
const SNAP_WARNING_HEIGHT = 700;
|
|
@@ -48,6 +48,8 @@ export function SnapViewV2({
|
|
|
48
48
|
onValidationError,
|
|
49
49
|
validationErrorFallback,
|
|
50
50
|
loadingOverlay,
|
|
51
|
+
initialRenderState,
|
|
52
|
+
onRenderStateChange,
|
|
51
53
|
}: {
|
|
52
54
|
snap: SnapPage;
|
|
53
55
|
handlers: SnapActionHandlers;
|
|
@@ -57,6 +59,8 @@ export function SnapViewV2({
|
|
|
57
59
|
validationErrorFallback?: ReactNode;
|
|
58
60
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
59
61
|
loadingOverlay?: ReactNode;
|
|
62
|
+
initialRenderState?: SnapRenderState;
|
|
63
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
60
64
|
}) {
|
|
61
65
|
const validation = useMemo(() => validateSnapResponse(snap), [snap]);
|
|
62
66
|
const valid = validation.valid;
|
|
@@ -85,6 +89,8 @@ export function SnapViewV2({
|
|
|
85
89
|
loading={loading}
|
|
86
90
|
appearance={appearance}
|
|
87
91
|
loadingOverlay={loadingOverlay}
|
|
92
|
+
initialRenderState={initialRenderState}
|
|
93
|
+
onRenderStateChange={onRenderStateChange}
|
|
88
94
|
/>
|
|
89
95
|
);
|
|
90
96
|
}
|
|
@@ -103,6 +109,8 @@ export function SnapCardV2({
|
|
|
103
109
|
actionError,
|
|
104
110
|
plain = false,
|
|
105
111
|
loadingOverlay,
|
|
112
|
+
initialRenderState,
|
|
113
|
+
onRenderStateChange,
|
|
106
114
|
}: {
|
|
107
115
|
snap: SnapPage;
|
|
108
116
|
handlers: SnapActionHandlers;
|
|
@@ -116,6 +124,10 @@ export function SnapCardV2({
|
|
|
116
124
|
plain?: boolean;
|
|
117
125
|
/** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
|
|
118
126
|
loadingOverlay?: ReactNode;
|
|
127
|
+
/** JSON-render local state used to seed this presenter mount. */
|
|
128
|
+
initialRenderState?: SnapRenderState;
|
|
129
|
+
/** Called with the full JSON-render local state after state changes. */
|
|
130
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
119
131
|
}) {
|
|
120
132
|
const isDark = appearance === "dark";
|
|
121
133
|
const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
|
|
@@ -208,6 +220,8 @@ export function SnapCardV2({
|
|
|
208
220
|
onValidationError={onValidationError}
|
|
209
221
|
validationErrorFallback={validationErrorFallback}
|
|
210
222
|
loadingOverlay={null}
|
|
223
|
+
initialRenderState={initialRenderState}
|
|
224
|
+
onRenderStateChange={onRenderStateChange}
|
|
211
225
|
/>
|
|
212
226
|
</div>
|
|
213
227
|
</div>
|
|
@@ -2,7 +2,12 @@ import type { ReactNode } from "react";
|
|
|
2
2
|
import type { ValidationResult } from "@farcaster/snap";
|
|
3
3
|
import { SPEC_VERSION_2 } from "@farcaster/snap";
|
|
4
4
|
import type { SnapNativeColors } from "./theme";
|
|
5
|
-
import type {
|
|
5
|
+
import type {
|
|
6
|
+
JsonValue,
|
|
7
|
+
SnapActionHandlers,
|
|
8
|
+
SnapPage,
|
|
9
|
+
SnapRenderState,
|
|
10
|
+
} from "./types";
|
|
6
11
|
import { useSnapTheme } from "./theme";
|
|
7
12
|
import { hexToRgba } from "./use-snap-palette";
|
|
8
13
|
import { SnapCardV1 } from "./v1/snap-view";
|
|
@@ -10,7 +15,12 @@ import { SnapCardV2 } from "./v2/snap-view";
|
|
|
10
15
|
|
|
11
16
|
// ─── Public types ──────────────────────────────────────
|
|
12
17
|
|
|
13
|
-
export type {
|
|
18
|
+
export type {
|
|
19
|
+
JsonValue,
|
|
20
|
+
SnapActionHandlers,
|
|
21
|
+
SnapPage,
|
|
22
|
+
SnapRenderState,
|
|
23
|
+
} from "./types";
|
|
14
24
|
|
|
15
25
|
// ─── Re-exports ───────────────────────────────────────
|
|
16
26
|
|
|
@@ -35,6 +45,8 @@ export function SnapCard({
|
|
|
35
45
|
forceExpanded,
|
|
36
46
|
expandButtonLabel,
|
|
37
47
|
onExpandPress,
|
|
48
|
+
initialRenderState,
|
|
49
|
+
onRenderStateChange,
|
|
38
50
|
}: {
|
|
39
51
|
snap: SnapPage;
|
|
40
52
|
handlers: SnapActionHandlers;
|
|
@@ -61,6 +73,10 @@ export function SnapCard({
|
|
|
61
73
|
expandButtonLabel?: string;
|
|
62
74
|
/** Called from the collapsed expand button instead of toggling internal state. */
|
|
63
75
|
onExpandPress?: () => void;
|
|
76
|
+
/** JSON-render local state used to seed this presenter mount. */
|
|
77
|
+
initialRenderState?: SnapRenderState;
|
|
78
|
+
/** Called with the full JSON-render local state after state changes. */
|
|
79
|
+
onRenderStateChange?: (state: SnapRenderState) => void;
|
|
64
80
|
}) {
|
|
65
81
|
if (snap.version === SPEC_VERSION_2) {
|
|
66
82
|
return (
|
|
@@ -80,6 +96,8 @@ export function SnapCard({
|
|
|
80
96
|
forceExpanded={forceExpanded}
|
|
81
97
|
expandButtonLabel={expandButtonLabel}
|
|
82
98
|
onExpandPress={onExpandPress}
|
|
99
|
+
initialRenderState={initialRenderState}
|
|
100
|
+
onRenderStateChange={onRenderStateChange}
|
|
83
101
|
/>
|
|
84
102
|
);
|
|
85
103
|
}
|
|
@@ -98,6 +116,8 @@ export function SnapCard({
|
|
|
98
116
|
forceExpanded={forceExpanded}
|
|
99
117
|
expandButtonLabel={expandButtonLabel}
|
|
100
118
|
onExpandPress={onExpandPress}
|
|
119
|
+
initialRenderState={initialRenderState}
|
|
120
|
+
onRenderStateChange={onRenderStateChange}
|
|
101
121
|
/>
|
|
102
122
|
);
|
|
103
123
|
}
|