@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.
- 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 -13
- package/dist/react/snap-view-core.js +90 -45
- 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 -44
- package/dist/react-native/types.d.ts +2 -13
- 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 -14
- package/dist/ui/catalog.js +15 -22
- 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 -14
- package/src/react/snap-view-core.tsx +144 -48
- 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 -48
- package/src/react-native/types.ts +4 -14
- 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 -25
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { Spec } from "@json-render/core";
|
|
4
|
+
import { createStateStore } from "@json-render/react";
|
|
4
5
|
import { snapJsonRenderCatalog } from "../ui/index.js";
|
|
5
6
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
6
7
|
import { SnapPreviewAccentProvider } from "./accent-context";
|
|
@@ -8,10 +9,11 @@ import { SnapVersionProvider } from "./snap-version-context";
|
|
|
8
9
|
import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
|
|
9
10
|
import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
|
|
10
11
|
import {
|
|
11
|
-
|
|
12
|
+
buildActionActivityStateChanges,
|
|
12
13
|
buildInitialRenderState,
|
|
13
14
|
cloneSnapRenderState,
|
|
14
15
|
getUnpresentedSnapEffects,
|
|
16
|
+
hasPendingSnapAction,
|
|
15
17
|
markSnapEffectsPresented,
|
|
16
18
|
type SnapRenderState,
|
|
17
19
|
} from "../render-state";
|
|
@@ -36,6 +38,12 @@ function optionalString(value: unknown): string | undefined {
|
|
|
36
38
|
return value ? String(value) : undefined;
|
|
37
39
|
}
|
|
38
40
|
|
|
41
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
42
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
43
|
+
? (value as Record<string, unknown>)
|
|
44
|
+
: undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
39
47
|
function withDefaultElementProps(spec: Spec): Spec {
|
|
40
48
|
if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
|
|
41
49
|
const elements = spec.elements as unknown as Record<
|
|
@@ -266,6 +274,8 @@ export function SnapLoadingOverlay({
|
|
|
266
274
|
|
|
267
275
|
return (
|
|
268
276
|
<div
|
|
277
|
+
data-snap-loading-overlay
|
|
278
|
+
data-snap-loading-active={active ? "true" : "false"}
|
|
269
279
|
style={{
|
|
270
280
|
position: "absolute",
|
|
271
281
|
inset: 0,
|
|
@@ -299,6 +309,20 @@ export function SnapLoadingOverlay({
|
|
|
299
309
|
}}
|
|
300
310
|
/>
|
|
301
311
|
<style>{`
|
|
312
|
+
[data-snap-view-root]:has([data-snap-action-pending-active="true"])
|
|
313
|
+
[data-snap-loading-overlay] {
|
|
314
|
+
opacity: 1 !important;
|
|
315
|
+
pointer-events: auto !important;
|
|
316
|
+
backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
317
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
318
|
+
}
|
|
319
|
+
[data-snap-card-surface]:has([data-snap-action-pending-active="true"])
|
|
320
|
+
> [data-snap-loading-overlay] {
|
|
321
|
+
opacity: 1 !important;
|
|
322
|
+
pointer-events: auto !important;
|
|
323
|
+
backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
324
|
+
-webkit-backdrop-filter: blur(10px) saturate(1.05) !important;
|
|
325
|
+
}
|
|
302
326
|
@keyframes snapViewSpin {
|
|
303
327
|
to { transform: rotate(360deg); }
|
|
304
328
|
}
|
|
@@ -314,6 +338,22 @@ export function SnapLoadingOverlay({
|
|
|
314
338
|
);
|
|
315
339
|
}
|
|
316
340
|
|
|
341
|
+
function SnapPendingActionOverlay({
|
|
342
|
+
appearance,
|
|
343
|
+
accentHex,
|
|
344
|
+
}: {
|
|
345
|
+
appearance: "light" | "dark";
|
|
346
|
+
accentHex: string;
|
|
347
|
+
}) {
|
|
348
|
+
return (
|
|
349
|
+
<SnapLoadingOverlay
|
|
350
|
+
appearance={appearance}
|
|
351
|
+
accentHex={accentHex}
|
|
352
|
+
active={false}
|
|
353
|
+
/>
|
|
354
|
+
);
|
|
355
|
+
}
|
|
356
|
+
|
|
317
357
|
const PALETTE = [
|
|
318
358
|
"gray",
|
|
319
359
|
"blue",
|
|
@@ -360,12 +400,31 @@ export function SnapViewCore({
|
|
|
360
400
|
[initialRenderState, spec.state, snap.theme?.accent],
|
|
361
401
|
);
|
|
362
402
|
|
|
403
|
+
const stateStore = useMemo(() => createStateStore(initialState), [
|
|
404
|
+
initialState,
|
|
405
|
+
]);
|
|
363
406
|
const stateRef = useRef<Record<string, unknown>>(initialState);
|
|
407
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
408
|
+
const pendingActionCountRef = useRef(0);
|
|
364
409
|
|
|
365
410
|
useEffect(() => {
|
|
366
411
|
stateRef.current = cloneSnapRenderState(initialState);
|
|
367
412
|
}, [initialState]);
|
|
368
413
|
|
|
414
|
+
useEffect(() => {
|
|
415
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
416
|
+
}, [onRenderStateChange]);
|
|
417
|
+
|
|
418
|
+
useEffect(
|
|
419
|
+
() =>
|
|
420
|
+
stateStore.subscribe(() => {
|
|
421
|
+
const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
|
|
422
|
+
stateRef.current = snapshot;
|
|
423
|
+
onRenderStateChangeRef.current?.(snapshot);
|
|
424
|
+
}),
|
|
425
|
+
[stateStore],
|
|
426
|
+
);
|
|
427
|
+
|
|
369
428
|
useEffect(() => {
|
|
370
429
|
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
371
430
|
if (!catalogResult.success) {
|
|
@@ -390,10 +449,6 @@ export function SnapViewCore({
|
|
|
390
449
|
confetti: 0,
|
|
391
450
|
fireworks: 0,
|
|
392
451
|
});
|
|
393
|
-
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
394
|
-
useEffect(() => {
|
|
395
|
-
onRenderStateChangeRef.current = onRenderStateChange;
|
|
396
|
-
}, [onRenderStateChange]);
|
|
397
452
|
useEffect(() => {
|
|
398
453
|
const effectsToPresent = getUnpresentedSnapEffects(
|
|
399
454
|
stateRef.current,
|
|
@@ -415,7 +470,10 @@ export function SnapViewCore({
|
|
|
415
470
|
}
|
|
416
471
|
|
|
417
472
|
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
418
|
-
|
|
473
|
+
const meta = recordValue(stateRef.current.__snapRender);
|
|
474
|
+
stateStore.update({
|
|
475
|
+
"/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
|
|
476
|
+
});
|
|
419
477
|
}
|
|
420
478
|
|
|
421
479
|
setEffectRunKeys((current) => ({
|
|
@@ -430,7 +488,7 @@ export function SnapViewCore({
|
|
|
430
488
|
? current.fireworks
|
|
431
489
|
: 0,
|
|
432
490
|
}));
|
|
433
|
-
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
491
|
+
}, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
|
|
434
492
|
|
|
435
493
|
const accentName = snap.theme?.accent ?? "purple";
|
|
436
494
|
|
|
@@ -449,6 +507,40 @@ export function SnapViewCore({
|
|
|
449
507
|
} as CSSProperties;
|
|
450
508
|
}, [accentName, appearance]);
|
|
451
509
|
|
|
510
|
+
const applyActionActivityState = useCallback(
|
|
511
|
+
(name: unknown, params: Record<string, unknown>, pending: boolean) => {
|
|
512
|
+
stateStore.update(
|
|
513
|
+
Object.fromEntries(
|
|
514
|
+
buildActionActivityStateChanges({
|
|
515
|
+
actionName: name,
|
|
516
|
+
params,
|
|
517
|
+
pending,
|
|
518
|
+
}).map(({ path, value }) => [path, value]),
|
|
519
|
+
),
|
|
520
|
+
);
|
|
521
|
+
},
|
|
522
|
+
[stateStore],
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
const setActionPending = useCallback(
|
|
526
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
527
|
+
pendingActionCountRef.current += 1;
|
|
528
|
+
applyActionActivityState(name, params, true);
|
|
529
|
+
},
|
|
530
|
+
[applyActionActivityState],
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
const setActionSettled = useCallback(
|
|
534
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
535
|
+
pendingActionCountRef.current = Math.max(
|
|
536
|
+
0,
|
|
537
|
+
pendingActionCountRef.current - 1,
|
|
538
|
+
);
|
|
539
|
+
applyActionActivityState(name, params, false);
|
|
540
|
+
},
|
|
541
|
+
[applyActionActivityState],
|
|
542
|
+
);
|
|
543
|
+
|
|
452
544
|
const handleAction = useCallback(
|
|
453
545
|
(name: unknown, params: unknown) => {
|
|
454
546
|
const inputs = (stateRef.current.inputs ?? {}) as Record<
|
|
@@ -456,30 +548,35 @@ export function SnapViewCore({
|
|
|
456
548
|
JsonValue
|
|
457
549
|
>;
|
|
458
550
|
const p = (params ?? {}) as Record<string, unknown>;
|
|
551
|
+
let result: unknown;
|
|
552
|
+
setActionPending(name, p);
|
|
553
|
+
|
|
459
554
|
switch (name) {
|
|
460
555
|
case "submit":
|
|
461
|
-
handlers.submit(String(p.target ?? ""), inputs);
|
|
556
|
+
result = handlers.submit(String(p.target ?? ""), inputs);
|
|
462
557
|
break;
|
|
463
558
|
case "open_url":
|
|
464
|
-
handlers.open_url(String(p.target ?? ""));
|
|
559
|
+
result = handlers.open_url(String(p.target ?? ""));
|
|
465
560
|
break;
|
|
466
561
|
case "open_snap":
|
|
467
|
-
handlers.open_snap(String(p.target ?? ""));
|
|
562
|
+
result = handlers.open_snap(String(p.target ?? ""));
|
|
468
563
|
break;
|
|
469
564
|
case "open_mini_app":
|
|
470
|
-
handlers.open_mini_app(String(p.target ?? ""));
|
|
565
|
+
result = handlers.open_mini_app(String(p.target ?? ""));
|
|
471
566
|
break;
|
|
472
567
|
case "view_cast":
|
|
473
|
-
handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
568
|
+
result = handlers.view_cast({ hash: String(p.hash ?? "") });
|
|
474
569
|
break;
|
|
475
570
|
case "view_profile":
|
|
476
|
-
handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
571
|
+
result = handlers.view_profile({ fid: Number(p.fid ?? 0) });
|
|
477
572
|
break;
|
|
478
573
|
case "view_channel":
|
|
479
|
-
handlers.view_channel({
|
|
574
|
+
result = handlers.view_channel({
|
|
575
|
+
channelKey: String(p.channelKey ?? ""),
|
|
576
|
+
});
|
|
480
577
|
break;
|
|
481
578
|
case "compose_cast":
|
|
482
|
-
handlers.compose_cast({
|
|
579
|
+
result = handlers.compose_cast({
|
|
483
580
|
text: p.text ? String(p.text) : undefined,
|
|
484
581
|
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
485
582
|
embeds: Array.isArray(p.embeds)
|
|
@@ -488,10 +585,10 @@ export function SnapViewCore({
|
|
|
488
585
|
});
|
|
489
586
|
break;
|
|
490
587
|
case "view_token":
|
|
491
|
-
handlers.view_token({ token: String(p.token ?? "") });
|
|
588
|
+
result = handlers.view_token({ token: String(p.token ?? "") });
|
|
492
589
|
break;
|
|
493
590
|
case "send_token":
|
|
494
|
-
handlers.send_token({
|
|
591
|
+
result = handlers.send_token({
|
|
495
592
|
token: String(p.token ?? ""),
|
|
496
593
|
amount: p.amount ? String(p.amount) : undefined,
|
|
497
594
|
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
@@ -501,13 +598,13 @@ export function SnapViewCore({
|
|
|
501
598
|
});
|
|
502
599
|
break;
|
|
503
600
|
case "swap_token":
|
|
504
|
-
handlers.swap_token({
|
|
601
|
+
result = handlers.swap_token({
|
|
505
602
|
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
506
603
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
507
604
|
});
|
|
508
605
|
break;
|
|
509
606
|
case "send_transaction":
|
|
510
|
-
handlers.send_transaction?.({
|
|
607
|
+
result = handlers.send_transaction?.({
|
|
511
608
|
chainId: String(p.chainId ?? ""),
|
|
512
609
|
to: String(p.to ?? ""),
|
|
513
610
|
data: optionalString(p.data),
|
|
@@ -518,36 +615,32 @@ export function SnapViewCore({
|
|
|
518
615
|
maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
|
|
519
616
|
});
|
|
520
617
|
break;
|
|
521
|
-
case "send_calls":
|
|
522
|
-
handlers.send_calls?.({
|
|
523
|
-
version: p.version === "1.0" ? "1.0" : undefined,
|
|
524
|
-
chainId: String(p.chainId ?? ""),
|
|
525
|
-
atomicRequired:
|
|
526
|
-
typeof p.atomicRequired === "boolean"
|
|
527
|
-
? p.atomicRequired
|
|
528
|
-
: undefined,
|
|
529
|
-
id: optionalString(p.id),
|
|
530
|
-
calls: Array.isArray(p.calls)
|
|
531
|
-
? p.calls.map((call) => {
|
|
532
|
-
const c = asRecord(call);
|
|
533
|
-
return {
|
|
534
|
-
to: optionalString(c.to),
|
|
535
|
-
data: optionalString(c.data),
|
|
536
|
-
value: optionalString(c.value),
|
|
537
|
-
};
|
|
538
|
-
})
|
|
539
|
-
: [],
|
|
540
|
-
});
|
|
541
|
-
break;
|
|
542
618
|
default:
|
|
543
619
|
break;
|
|
544
620
|
}
|
|
621
|
+
|
|
622
|
+
if (result instanceof Promise) {
|
|
623
|
+
void result.finally(() => {
|
|
624
|
+
setActionSettled(name, p);
|
|
625
|
+
}).catch(() => {});
|
|
626
|
+
} else {
|
|
627
|
+
setActionSettled(name, p);
|
|
628
|
+
}
|
|
629
|
+
return result;
|
|
545
630
|
},
|
|
546
|
-
[handlers],
|
|
631
|
+
[handlers, setActionPending, setActionSettled],
|
|
547
632
|
);
|
|
548
633
|
|
|
549
634
|
return (
|
|
550
|
-
<div
|
|
635
|
+
<div
|
|
636
|
+
data-snap-view-root
|
|
637
|
+
style={{ position: "relative", width: "100%" }}
|
|
638
|
+
onClickCapture={(event) => {
|
|
639
|
+
if (!hasPendingSnapAction(stateRef.current)) return;
|
|
640
|
+
event.preventDefault();
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
}}
|
|
643
|
+
>
|
|
551
644
|
{showConfetti && effectRunKeys.confetti > 0 && (
|
|
552
645
|
<ConfettiOverlay key={effectRunKeys.confetti} />
|
|
553
646
|
)}
|
|
@@ -573,14 +666,17 @@ export function SnapViewCore({
|
|
|
573
666
|
<SnapCatalogView
|
|
574
667
|
key={pageKey}
|
|
575
668
|
spec={spec}
|
|
576
|
-
|
|
669
|
+
store={stateStore}
|
|
577
670
|
loading={false}
|
|
578
|
-
onStateChange={(changes) => {
|
|
579
|
-
applyStatePaths(stateRef.current, changes);
|
|
580
|
-
onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
|
|
581
|
-
}}
|
|
582
671
|
onAction={handleAction}
|
|
583
|
-
|
|
672
|
+
>
|
|
673
|
+
{loadingOverlay === undefined ? (
|
|
674
|
+
<SnapPendingActionOverlay
|
|
675
|
+
appearance={appearance}
|
|
676
|
+
accentHex={accentHex}
|
|
677
|
+
/>
|
|
678
|
+
) : null}
|
|
679
|
+
</SnapCatalogView>
|
|
584
680
|
</SnapVersionProvider>
|
|
585
681
|
</SnapPreviewAccentProvider>
|
|
586
682
|
</div>
|
|
@@ -34,6 +34,7 @@ export function SnapActionButton({
|
|
|
34
34
|
const label = String(props.label ?? "Action");
|
|
35
35
|
const variant = String(props.variant ?? "secondary");
|
|
36
36
|
const isPrimary = variant === "primary";
|
|
37
|
+
const disabled = props.disabled === true;
|
|
37
38
|
const iconName = props.icon ? String(props.icon) : undefined;
|
|
38
39
|
|
|
39
40
|
const textColor = isPrimary ? "#fff" : colors.text;
|
|
@@ -53,10 +54,13 @@ export function SnapActionButton({
|
|
|
53
54
|
isPrimary ? styles.btnDefault : styles.btnOther,
|
|
54
55
|
isPrimary
|
|
55
56
|
? { backgroundColor: pressed ? accentHex + "DD" : accentHex }
|
|
56
|
-
|
|
57
|
+
: { backgroundColor: pressed ? colors.mutedHover : colors.muted },
|
|
57
58
|
pressed && styles.pressed,
|
|
59
|
+
disabled && styles.disabled,
|
|
58
60
|
]}
|
|
61
|
+
disabled={disabled}
|
|
59
62
|
onPress={() => {
|
|
63
|
+
if (disabled) return;
|
|
60
64
|
if (runPaginatorAction(stateStore, paginatorAction)) return;
|
|
61
65
|
void (async () => {
|
|
62
66
|
try {
|
|
@@ -114,4 +118,5 @@ const styles = StyleSheet.create({
|
|
|
114
118
|
paddingVertical: 6,
|
|
115
119
|
},
|
|
116
120
|
pressed: { opacity: 0.88 },
|
|
121
|
+
disabled: { opacity: 0.62 },
|
|
117
122
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
|
+
import { createStateStore } from "@json-render/react-native";
|
|
2
3
|
import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
|
|
3
4
|
import { SnapCatalogView } from "./catalog-renderer";
|
|
4
5
|
import { ConfettiOverlay } from "./confetti-overlay";
|
|
@@ -21,10 +22,11 @@ import {
|
|
|
21
22
|
type PaletteColor,
|
|
22
23
|
} from "@farcaster/snap";
|
|
23
24
|
import {
|
|
24
|
-
|
|
25
|
+
buildActionActivityStateChanges,
|
|
25
26
|
buildInitialRenderState,
|
|
26
27
|
cloneSnapRenderState,
|
|
27
28
|
getUnpresentedSnapEffects,
|
|
29
|
+
hasPendingSnapAction,
|
|
28
30
|
markSnapEffectsPresented,
|
|
29
31
|
type SnapRenderState,
|
|
30
32
|
} from "../render-state";
|
|
@@ -40,6 +42,12 @@ function optionalString(value: unknown): string | undefined {
|
|
|
40
42
|
return value ? String(value) : undefined;
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
function recordValue(value: unknown): Record<string, unknown> | undefined {
|
|
46
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
47
|
+
? (value as Record<string, unknown>)
|
|
48
|
+
: undefined;
|
|
49
|
+
}
|
|
50
|
+
|
|
43
51
|
function withDefaultElementProps(spec: Spec): Spec {
|
|
44
52
|
if (!spec || typeof spec !== "object" || !("elements" in spec)) return spec;
|
|
45
53
|
const elements = spec.elements as unknown as Record<
|
|
@@ -114,12 +122,34 @@ export function SnapViewCoreInner({
|
|
|
114
122
|
[initialRenderState, spec.state, snap.theme?.accent],
|
|
115
123
|
);
|
|
116
124
|
|
|
125
|
+
const stateStore = useMemo(() => createStateStore(initialState), [
|
|
126
|
+
initialState,
|
|
127
|
+
]);
|
|
117
128
|
const stateRef = useRef<Record<string, unknown>>(initialState);
|
|
129
|
+
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
130
|
+
const pendingActionCountRef = useRef(0);
|
|
131
|
+
const [hasPendingAction, setHasPendingAction] = useState(false);
|
|
132
|
+
const [actionActivityVersion, setActionActivityVersion] = useState(0);
|
|
118
133
|
|
|
119
134
|
useEffect(() => {
|
|
120
135
|
stateRef.current = cloneSnapRenderState(initialState);
|
|
121
136
|
}, [initialState]);
|
|
122
137
|
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
onRenderStateChangeRef.current = onRenderStateChange;
|
|
140
|
+
}, [onRenderStateChange]);
|
|
141
|
+
|
|
142
|
+
useEffect(
|
|
143
|
+
() =>
|
|
144
|
+
stateStore.subscribe(() => {
|
|
145
|
+
const snapshot = cloneSnapRenderState(stateStore.getSnapshot());
|
|
146
|
+
stateRef.current = snapshot;
|
|
147
|
+
setHasPendingAction(hasPendingSnapAction(snapshot));
|
|
148
|
+
onRenderStateChangeRef.current?.(snapshot);
|
|
149
|
+
}),
|
|
150
|
+
[stateStore],
|
|
151
|
+
);
|
|
152
|
+
|
|
123
153
|
useEffect(() => {
|
|
124
154
|
const catalogResult = snapJsonRenderCatalog.validate(spec);
|
|
125
155
|
if (!catalogResult.success) {
|
|
@@ -144,10 +174,6 @@ export function SnapViewCoreInner({
|
|
|
144
174
|
confetti: 0,
|
|
145
175
|
fireworks: 0,
|
|
146
176
|
});
|
|
147
|
-
const onRenderStateChangeRef = useRef(onRenderStateChange);
|
|
148
|
-
useEffect(() => {
|
|
149
|
-
onRenderStateChangeRef.current = onRenderStateChange;
|
|
150
|
-
}, [onRenderStateChange]);
|
|
151
177
|
useEffect(() => {
|
|
152
178
|
const effectsToPresent = getUnpresentedSnapEffects(
|
|
153
179
|
stateRef.current,
|
|
@@ -169,7 +195,10 @@ export function SnapViewCoreInner({
|
|
|
169
195
|
}
|
|
170
196
|
|
|
171
197
|
if (markSnapEffectsPresented(stateRef.current, effectsToPresent)) {
|
|
172
|
-
|
|
198
|
+
const meta = recordValue(stateRef.current.__snapRender);
|
|
199
|
+
stateStore.update({
|
|
200
|
+
"/__snapRender/presentedEffects": meta?.presentedEffects ?? [],
|
|
201
|
+
});
|
|
173
202
|
}
|
|
174
203
|
|
|
175
204
|
setEffectRunKeys((current) => ({
|
|
@@ -184,49 +213,92 @@ export function SnapViewCoreInner({
|
|
|
184
213
|
? current.fireworks
|
|
185
214
|
: 0,
|
|
186
215
|
}));
|
|
187
|
-
}, [initialState, showConfetti, showFireworks, snapEffects]);
|
|
216
|
+
}, [initialState, showConfetti, showFireworks, snapEffects, stateStore]);
|
|
188
217
|
|
|
189
218
|
const handlersRef = useRef(handlers);
|
|
190
219
|
handlersRef.current = handlers;
|
|
191
220
|
|
|
221
|
+
const applyActionActivityState = useCallback(
|
|
222
|
+
(name: unknown, params: Record<string, unknown>, pending: boolean) => {
|
|
223
|
+
stateStore.update(
|
|
224
|
+
Object.fromEntries(
|
|
225
|
+
buildActionActivityStateChanges({
|
|
226
|
+
actionName: name,
|
|
227
|
+
params,
|
|
228
|
+
pending,
|
|
229
|
+
}).map(({ path, value }) => [path, value]),
|
|
230
|
+
),
|
|
231
|
+
);
|
|
232
|
+
},
|
|
233
|
+
[stateStore],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const setActionPending = useCallback(
|
|
237
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
238
|
+
pendingActionCountRef.current += 1;
|
|
239
|
+
setHasPendingAction(true);
|
|
240
|
+
setActionActivityVersion((version) => version + 1);
|
|
241
|
+
applyActionActivityState(name, params, true);
|
|
242
|
+
},
|
|
243
|
+
[applyActionActivityState],
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const setActionSettled = useCallback(
|
|
247
|
+
(name: unknown, params: Record<string, unknown>) => {
|
|
248
|
+
pendingActionCountRef.current = Math.max(
|
|
249
|
+
0,
|
|
250
|
+
pendingActionCountRef.current - 1,
|
|
251
|
+
);
|
|
252
|
+
applyActionActivityState(name, params, false);
|
|
253
|
+
if (pendingActionCountRef.current === 0) {
|
|
254
|
+
setHasPendingAction(false);
|
|
255
|
+
}
|
|
256
|
+
setActionActivityVersion((version) => version + 1);
|
|
257
|
+
},
|
|
258
|
+
[applyActionActivityState],
|
|
259
|
+
);
|
|
260
|
+
|
|
192
261
|
const handleAction = useCallback((name: unknown, params: unknown) => {
|
|
193
262
|
const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
|
|
194
263
|
const p = (params ?? {}) as Record<string, unknown>;
|
|
195
264
|
const h = handlersRef.current;
|
|
265
|
+
let result: unknown;
|
|
266
|
+
setActionPending(name, p);
|
|
267
|
+
|
|
196
268
|
switch (name) {
|
|
197
269
|
case "submit":
|
|
198
|
-
h.submit(String(p.target ?? ""), inputs);
|
|
270
|
+
result = h.submit(String(p.target ?? ""), inputs);
|
|
199
271
|
break;
|
|
200
272
|
case "open_url":
|
|
201
|
-
h.open_url(String(p.target ?? ""));
|
|
273
|
+
result = h.open_url(String(p.target ?? ""));
|
|
202
274
|
break;
|
|
203
275
|
case "open_snap":
|
|
204
|
-
h.open_snap(String(p.target ?? ""));
|
|
276
|
+
result = h.open_snap(String(p.target ?? ""));
|
|
205
277
|
break;
|
|
206
278
|
case "open_mini_app":
|
|
207
|
-
h.open_mini_app(String(p.target ?? ""));
|
|
279
|
+
result = h.open_mini_app(String(p.target ?? ""));
|
|
208
280
|
break;
|
|
209
281
|
case "view_cast":
|
|
210
|
-
h.view_cast({ hash: String(p.hash ?? "") });
|
|
282
|
+
result = h.view_cast({ hash: String(p.hash ?? "") });
|
|
211
283
|
break;
|
|
212
284
|
case "view_profile":
|
|
213
|
-
h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
285
|
+
result = h.view_profile({ fid: Number(p.fid ?? 0) });
|
|
214
286
|
break;
|
|
215
287
|
case "view_channel":
|
|
216
|
-
h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
288
|
+
result = h.view_channel({ channelKey: String(p.channelKey ?? "") });
|
|
217
289
|
break;
|
|
218
290
|
case "compose_cast":
|
|
219
|
-
h.compose_cast({
|
|
291
|
+
result = h.compose_cast({
|
|
220
292
|
text: p.text ? String(p.text) : undefined,
|
|
221
293
|
channelKey: p.channelKey ? String(p.channelKey) : undefined,
|
|
222
294
|
embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
|
|
223
295
|
});
|
|
224
296
|
break;
|
|
225
297
|
case "view_token":
|
|
226
|
-
h.view_token({ token: String(p.token ?? "") });
|
|
298
|
+
result = h.view_token({ token: String(p.token ?? "") });
|
|
227
299
|
break;
|
|
228
300
|
case "send_token":
|
|
229
|
-
h.send_token({
|
|
301
|
+
result = h.send_token({
|
|
230
302
|
token: String(p.token ?? ""),
|
|
231
303
|
amount: p.amount ? String(p.amount) : undefined,
|
|
232
304
|
recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
|
|
@@ -236,13 +308,13 @@ export function SnapViewCoreInner({
|
|
|
236
308
|
});
|
|
237
309
|
break;
|
|
238
310
|
case "swap_token":
|
|
239
|
-
h.swap_token({
|
|
311
|
+
result = h.swap_token({
|
|
240
312
|
sellToken: p.sellToken ? String(p.sellToken) : undefined,
|
|
241
313
|
buyToken: p.buyToken ? String(p.buyToken) : undefined,
|
|
242
314
|
});
|
|
243
315
|
break;
|
|
244
316
|
case "send_transaction":
|
|
245
|
-
h.send_transaction?.({
|
|
317
|
+
result = h.send_transaction?.({
|
|
246
318
|
chainId: String(p.chainId ?? ""),
|
|
247
319
|
to: String(p.to ?? ""),
|
|
248
320
|
data: optionalString(p.data),
|
|
@@ -253,35 +325,33 @@ export function SnapViewCoreInner({
|
|
|
253
325
|
maxPriorityFeePerGas: optionalString(p.maxPriorityFeePerGas),
|
|
254
326
|
});
|
|
255
327
|
break;
|
|
256
|
-
case "send_calls":
|
|
257
|
-
h.send_calls?.({
|
|
258
|
-
version: p.version === "1.0" ? "1.0" : undefined,
|
|
259
|
-
chainId: String(p.chainId ?? ""),
|
|
260
|
-
atomicRequired:
|
|
261
|
-
typeof p.atomicRequired === "boolean"
|
|
262
|
-
? p.atomicRequired
|
|
263
|
-
: undefined,
|
|
264
|
-
id: optionalString(p.id),
|
|
265
|
-
calls: Array.isArray(p.calls)
|
|
266
|
-
? p.calls.map((call) => {
|
|
267
|
-
const c = asRecord(call);
|
|
268
|
-
return {
|
|
269
|
-
to: optionalString(c.to),
|
|
270
|
-
data: optionalString(c.data),
|
|
271
|
-
value: optionalString(c.value),
|
|
272
|
-
};
|
|
273
|
-
})
|
|
274
|
-
: [],
|
|
275
|
-
});
|
|
276
|
-
break;
|
|
277
328
|
default:
|
|
278
329
|
break;
|
|
279
330
|
}
|
|
280
|
-
|
|
331
|
+
|
|
332
|
+
if (result instanceof Promise) {
|
|
333
|
+
void result.finally(() => {
|
|
334
|
+
setActionSettled(name, p);
|
|
335
|
+
}).catch(() => {});
|
|
336
|
+
} else {
|
|
337
|
+
setActionSettled(name, p);
|
|
338
|
+
}
|
|
339
|
+
return result;
|
|
340
|
+
}, [setActionPending, setActionSettled]);
|
|
341
|
+
|
|
342
|
+
const showLoadingOverlay =
|
|
343
|
+
loading ||
|
|
344
|
+
hasPendingAction ||
|
|
345
|
+
(actionActivityVersion >= 0 && pendingActionCountRef.current > 0);
|
|
281
346
|
|
|
282
347
|
return (
|
|
283
|
-
<View
|
|
284
|
-
{
|
|
348
|
+
<View
|
|
349
|
+
style={styles.container}
|
|
350
|
+
onStartShouldSetResponderCapture={() =>
|
|
351
|
+
hasPendingSnapAction(stateRef.current)
|
|
352
|
+
}
|
|
353
|
+
>
|
|
354
|
+
{showLoadingOverlay ? (
|
|
285
355
|
loadingOverlay === undefined ? (
|
|
286
356
|
<SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
|
|
287
357
|
) : (
|
|
@@ -292,12 +362,8 @@ export function SnapViewCoreInner({
|
|
|
292
362
|
<SnapCatalogView
|
|
293
363
|
key={pageKey}
|
|
294
364
|
spec={spec}
|
|
295
|
-
|
|
365
|
+
store={stateStore}
|
|
296
366
|
loading={false}
|
|
297
|
-
onStateChange={(changes) => {
|
|
298
|
-
applyStatePaths(stateRef.current, changes);
|
|
299
|
-
onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
|
|
300
|
-
}}
|
|
301
367
|
onAction={handleAction}
|
|
302
368
|
/>
|
|
303
369
|
</SnapVersionProvider>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Spec } from "@json-render/core";
|
|
2
2
|
import type { SnapRenderState } from "../render-state";
|
|
3
|
+
import type { SnapTransactionResult } from "../schemas";
|
|
3
4
|
|
|
4
5
|
export type { SnapRenderState };
|
|
5
6
|
|
|
@@ -29,18 +30,6 @@ export type SnapSendTransactionParams = {
|
|
|
29
30
|
maxPriorityFeePerGas?: string;
|
|
30
31
|
};
|
|
31
32
|
|
|
32
|
-
export type SnapSendCallsParams = {
|
|
33
|
-
version?: "1.0";
|
|
34
|
-
chainId: string;
|
|
35
|
-
atomicRequired?: boolean;
|
|
36
|
-
id?: string;
|
|
37
|
-
calls: Array<{
|
|
38
|
-
to?: string;
|
|
39
|
-
data?: string;
|
|
40
|
-
value?: string;
|
|
41
|
-
}>;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
33
|
export type SnapActionHandlers = {
|
|
45
34
|
submit: (target: string, inputs: Record<string, JsonValue>) => void;
|
|
46
35
|
open_url: (target: string) => void;
|
|
@@ -62,6 +51,7 @@ export type SnapActionHandlers = {
|
|
|
62
51
|
recipientAddress?: string;
|
|
63
52
|
}) => void;
|
|
64
53
|
swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
|
|
65
|
-
send_transaction?: (
|
|
66
|
-
|
|
54
|
+
send_transaction?: (
|
|
55
|
+
params: SnapSendTransactionParams,
|
|
56
|
+
) => void | Promise<void | SnapTransactionResult>;
|
|
67
57
|
};
|