@farcaster/snap 1.6.0 → 1.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.
Files changed (65) hide show
  1. package/dist/dataStore.d.ts +1 -4
  2. package/dist/dataStore.js +4 -17
  3. package/dist/index.d.ts +1 -1
  4. package/dist/react-native/catalog-renderer.d.ts +5 -0
  5. package/dist/react-native/catalog-renderer.js +36 -0
  6. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  7. package/dist/react-native/components/snap-action-button.js +68 -0
  8. package/dist/react-native/components/snap-badge.d.ts +2 -0
  9. package/dist/react-native/components/snap-badge.js +38 -0
  10. package/dist/react-native/components/snap-icon.d.ts +5 -0
  11. package/dist/react-native/components/snap-icon.js +56 -0
  12. package/dist/react-native/components/snap-image.d.ts +2 -0
  13. package/dist/react-native/components/snap-image.js +24 -0
  14. package/dist/react-native/components/snap-input.d.ts +2 -0
  15. package/dist/react-native/components/snap-input.js +36 -0
  16. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  17. package/dist/react-native/components/snap-item-group.js +23 -0
  18. package/dist/react-native/components/snap-item.d.ts +5 -0
  19. package/dist/react-native/components/snap-item.js +45 -0
  20. package/dist/react-native/components/snap-progress.d.ts +2 -0
  21. package/dist/react-native/components/snap-progress.js +26 -0
  22. package/dist/react-native/components/snap-separator.d.ts +2 -0
  23. package/dist/react-native/components/snap-separator.js +23 -0
  24. package/dist/react-native/components/snap-slider.d.ts +2 -0
  25. package/dist/react-native/components/snap-slider.js +42 -0
  26. package/dist/react-native/components/snap-stack.d.ts +5 -0
  27. package/dist/react-native/components/snap-stack.js +49 -0
  28. package/dist/react-native/components/snap-switch.d.ts +2 -0
  29. package/dist/react-native/components/snap-switch.js +30 -0
  30. package/dist/react-native/components/snap-text.d.ts +2 -0
  31. package/dist/react-native/components/snap-text.js +37 -0
  32. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  33. package/dist/react-native/components/snap-toggle-group.js +100 -0
  34. package/dist/react-native/index.d.ts +52 -0
  35. package/dist/react-native/index.js +155 -0
  36. package/dist/react-native/theme.d.ts +21 -0
  37. package/dist/react-native/theme.js +37 -0
  38. package/dist/react-native/use-snap-palette.d.ts +13 -0
  39. package/dist/react-native/use-snap-palette.js +48 -0
  40. package/dist/ui/badge.d.ts +2 -2
  41. package/dist/ui/button.d.ts +2 -2
  42. package/dist/ui/catalog.d.ts +7 -7
  43. package/dist/ui/icon.d.ts +2 -2
  44. package/dist/ui/schema.d.ts +1 -1
  45. package/package.json +7 -2
  46. package/src/dataStore.ts +5 -29
  47. package/src/index.ts +0 -1
  48. package/src/react-native/catalog-renderer.tsx +37 -0
  49. package/src/react-native/components/snap-action-button.tsx +92 -0
  50. package/src/react-native/components/snap-badge.tsx +57 -0
  51. package/src/react-native/components/snap-icon.tsx +102 -0
  52. package/src/react-native/components/snap-image.tsx +38 -0
  53. package/src/react-native/components/snap-input.tsx +57 -0
  54. package/src/react-native/components/snap-item-group.tsx +43 -0
  55. package/src/react-native/components/snap-item.tsx +70 -0
  56. package/src/react-native/components/snap-progress.tsx +40 -0
  57. package/src/react-native/components/snap-separator.tsx +32 -0
  58. package/src/react-native/components/snap-slider.tsx +82 -0
  59. package/src/react-native/components/snap-stack.tsx +66 -0
  60. package/src/react-native/components/snap-switch.tsx +45 -0
  61. package/src/react-native/components/snap-text.tsx +53 -0
  62. package/src/react-native/components/snap-toggle-group.tsx +128 -0
  63. package/src/react-native/index.tsx +267 -0
  64. package/src/react-native/theme.tsx +73 -0
  65. package/src/react-native/use-snap-palette.ts +64 -0
@@ -0,0 +1,82 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import Slider from "@react-native-community/slider";
4
+ import { StyleSheet, Text, View } from "react-native";
5
+ import { useSnapPalette } from "../use-snap-palette";
6
+ import { useSnapTheme } from "../theme";
7
+
8
+ export function SnapSlider({
9
+ element: { props },
10
+ }: ComponentRenderProps<Record<string, unknown>>) {
11
+ const { get, set } = useStateStore();
12
+ const { accentHex } = useSnapPalette();
13
+ const { colors } = useSnapTheme();
14
+ const name = String(props.name ?? "slider");
15
+ const path = `/inputs/${name}`;
16
+ const min = Number(props.min ?? 0);
17
+ const max = Number(props.max ?? 100);
18
+ const step = props.step != null ? Number(props.step) : 1;
19
+ const fallback =
20
+ props.defaultValue != null ? Number(props.defaultValue) : (min + max) / 2;
21
+ const raw = get(path);
22
+ const value =
23
+ raw === undefined || raw === null ? fallback : Number(raw);
24
+ const clamped = Number.isFinite(value)
25
+ ? Math.min(max, Math.max(min, value))
26
+ : fallback;
27
+
28
+ const label = props.label != null ? String(props.label) : null;
29
+ const minLabel = props.minLabel != null ? String(props.minLabel) : null;
30
+ const maxLabel = props.maxLabel != null ? String(props.maxLabel) : null;
31
+
32
+ return (
33
+ <View style={styles.wrap}>
34
+ {label ? (
35
+ <View style={styles.labelRow}>
36
+ <Text style={[styles.label, { color: colors.text }]}>{label}</Text>
37
+ <Text style={[styles.valueText, { color: colors.textSecondary }]}>
38
+ {String(Math.round(clamped))}
39
+ </Text>
40
+ </View>
41
+ ) : null}
42
+ <Slider
43
+ style={styles.slider}
44
+ minimumValue={min}
45
+ maximumValue={max}
46
+ step={step > 0 ? step : 1}
47
+ value={clamped}
48
+ onValueChange={(v) => set(path, v)}
49
+ minimumTrackTintColor={accentHex}
50
+ maximumTrackTintColor={colors.muted}
51
+ thumbTintColor={accentHex}
52
+ />
53
+ {minLabel != null || maxLabel != null ? (
54
+ <View style={styles.minMaxRow}>
55
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
56
+ {minLabel ?? String(min)}
57
+ </Text>
58
+ <Text style={[styles.minMax, { color: colors.textSecondary }]}>
59
+ {maxLabel ?? String(max)}
60
+ </Text>
61
+ </View>
62
+ ) : null}
63
+ </View>
64
+ );
65
+ }
66
+
67
+ const styles = StyleSheet.create({
68
+ wrap: { width: "100%", gap: 6 },
69
+ labelRow: {
70
+ flexDirection: "row",
71
+ justifyContent: "space-between",
72
+ alignItems: "center",
73
+ },
74
+ label: { fontSize: 13, fontWeight: "500", flex: 1 },
75
+ valueText: { fontSize: 13 },
76
+ slider: { width: "100%", height: 40 },
77
+ minMaxRow: {
78
+ flexDirection: "row",
79
+ justifyContent: "space-between",
80
+ },
81
+ minMax: { fontSize: 12 },
82
+ });
@@ -0,0 +1,66 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import type { ReactNode } from "react";
3
+ import { StyleSheet, View } from "react-native";
4
+
5
+ const VGAP: Record<string, number> = {
6
+ none: 0,
7
+ sm: 8,
8
+ md: 16,
9
+ lg: 24,
10
+ };
11
+
12
+ const HGAP: Record<string, number> = {
13
+ none: 0,
14
+ sm: 4,
15
+ md: 8,
16
+ lg: 12,
17
+ };
18
+
19
+ const JUSTIFY: Record<string, "flex-start" | "center" | "flex-end" | "space-between" | "space-around"> = {
20
+ start: "flex-start",
21
+ center: "center",
22
+ end: "flex-end",
23
+ between: "space-between",
24
+ around: "space-around",
25
+ };
26
+
27
+ export function SnapStack({
28
+ element: { props },
29
+ children,
30
+ }: ComponentRenderProps<Record<string, unknown>> & { children?: ReactNode }) {
31
+ const direction = String(props.direction ?? "vertical");
32
+ const rawGap = props.gap;
33
+ const isHorizontal = direction === "horizontal";
34
+ const gapMap = isHorizontal ? HGAP : VGAP;
35
+ const gap =
36
+ typeof rawGap === "number"
37
+ ? rawGap
38
+ : typeof rawGap === "string" && rawGap in gapMap
39
+ ? gapMap[rawGap]!
40
+ : isHorizontal ? HGAP.md! : VGAP.md!;
41
+ const justify = props.justify ? JUSTIFY[String(props.justify)] : undefined;
42
+
43
+ return (
44
+ <View
45
+ style={[
46
+ styles.stack,
47
+ isHorizontal ? styles.horizontal : undefined,
48
+ { gap },
49
+ justify ? { justifyContent: justify } : undefined,
50
+ ]}
51
+ >
52
+ {children}
53
+ </View>
54
+ );
55
+ }
56
+
57
+ const styles = StyleSheet.create({
58
+ stack: {
59
+ width: "100%",
60
+ },
61
+ horizontal: {
62
+ flexDirection: "row",
63
+ alignItems: "center",
64
+ flexWrap: "wrap",
65
+ },
66
+ });
@@ -0,0 +1,45 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import { StyleSheet, Switch, Text, View } from "react-native";
4
+ import { useSnapPalette } from "../use-snap-palette";
5
+ import { useSnapTheme } from "../theme";
6
+
7
+ export function SnapSwitch({
8
+ element: { props },
9
+ }: ComponentRenderProps<Record<string, unknown>>) {
10
+ const { get, set } = useStateStore();
11
+ const { accentHex } = useSnapPalette();
12
+ const { colors } = useSnapTheme();
13
+ const name = String(props.name ?? "switch");
14
+ const path = `/inputs/${name}`;
15
+ const label = props.label ? String(props.label) : undefined;
16
+ const fallback = Boolean(props.defaultChecked ?? false);
17
+ const raw = get(path);
18
+ const checked = raw === undefined || raw === null ? fallback : Boolean(raw);
19
+
20
+ return (
21
+ <View style={styles.row}>
22
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
23
+ <Switch
24
+ value={checked}
25
+ onValueChange={(v) => set(path, v)}
26
+ trackColor={{ false: colors.border, true: accentHex }}
27
+ thumbColor="#fff"
28
+ />
29
+ </View>
30
+ );
31
+ }
32
+
33
+ const styles = StyleSheet.create({
34
+ row: {
35
+ flexDirection: "row",
36
+ alignItems: "center",
37
+ justifyContent: "space-between",
38
+ gap: 12,
39
+ },
40
+ label: {
41
+ fontSize: 14,
42
+ fontWeight: "400",
43
+ flex: 1,
44
+ },
45
+ });
@@ -0,0 +1,53 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { StyleSheet, Text, View } from "react-native";
3
+ import { useSnapTheme } from "../theme";
4
+
5
+ const SIZE_STYLES: Record<string, { fontSize: number; lineHeight?: number; fontWeight?: "400" | "500" | "600" | "700" }> = {
6
+ lg: { fontSize: 20, fontWeight: "700" },
7
+ md: { fontSize: 16, lineHeight: 24 },
8
+ sm: { fontSize: 13 },
9
+ };
10
+
11
+ const WEIGHT_MAP: Record<string, "400" | "500" | "600" | "700"> = {
12
+ bold: "700",
13
+ medium: "500",
14
+ normal: "400",
15
+ };
16
+
17
+ export function SnapText({
18
+ element: { props },
19
+ }: ComponentRenderProps<Record<string, unknown>>) {
20
+ const { colors } = useSnapTheme();
21
+ const content = String(props.content ?? "");
22
+ const size = String(props.size ?? "md");
23
+ const weight = props.weight ? String(props.weight) : undefined;
24
+ const align = (props.align as "left" | "center" | "right" | undefined) ?? undefined;
25
+
26
+ const sizeStyle = SIZE_STYLES[size] ?? SIZE_STYLES.md;
27
+ const resolvedWeight = weight ? WEIGHT_MAP[weight] : sizeStyle?.fontWeight;
28
+ const textAlign = align === "center" ? "center" : align === "right" ? "right" : "left";
29
+
30
+ return (
31
+ <View style={styles.wrap}>
32
+ <Text
33
+ style={[
34
+ styles.base,
35
+ {
36
+ color: colors.text,
37
+ fontSize: sizeStyle!.fontSize,
38
+ lineHeight: sizeStyle!.lineHeight,
39
+ fontWeight: resolvedWeight,
40
+ textAlign,
41
+ },
42
+ ]}
43
+ >
44
+ {content}
45
+ </Text>
46
+ </View>
47
+ );
48
+ }
49
+
50
+ const styles = StyleSheet.create({
51
+ wrap: { flex: 1, width: "100%" },
52
+ base: {},
53
+ });
@@ -0,0 +1,128 @@
1
+ import type { ComponentRenderProps } from "@json-render/react-native";
2
+ import { useStateStore } from "@json-render/react-native";
3
+ import { Pressable, StyleSheet, Text, View } from "react-native";
4
+ import { useSnapPalette } from "../use-snap-palette";
5
+ import { useSnapTheme } from "../theme";
6
+
7
+ export function SnapToggleGroup({
8
+ element: { props },
9
+ }: ComponentRenderProps<Record<string, unknown>>) {
10
+ const { get, set } = useStateStore();
11
+ const { accentHex } = useSnapPalette();
12
+ const { colors } = useSnapTheme();
13
+ const name = String(props.name ?? "toggle_group");
14
+ const path = `/inputs/${name}`;
15
+ const label = props.label ? String(props.label) : undefined;
16
+ const isMultiple = Boolean(props.multiple);
17
+ const orientation = String(props.orientation ?? "horizontal");
18
+ const options = Array.isArray(props.options)
19
+ ? (props.options as string[])
20
+ : [];
21
+
22
+ const raw = get(path);
23
+ const defaultValue = props.defaultValue;
24
+
25
+ const selected = (() => {
26
+ if (raw !== undefined && raw !== null) {
27
+ return isMultiple
28
+ ? Array.isArray(raw)
29
+ ? (raw as string[])
30
+ : []
31
+ : typeof raw === "string"
32
+ ? [raw]
33
+ : [];
34
+ }
35
+ if (defaultValue !== undefined) {
36
+ return Array.isArray(defaultValue)
37
+ ? (defaultValue as string[])
38
+ : [String(defaultValue)];
39
+ }
40
+ return [];
41
+ })();
42
+
43
+ const isVertical = orientation === "vertical";
44
+
45
+ const handlePress = (opt: string) => {
46
+ if (isMultiple) {
47
+ const next = selected.includes(opt)
48
+ ? selected.filter((s) => s !== opt)
49
+ : [...selected, opt];
50
+ set(path, next);
51
+ } else {
52
+ if (opt && opt !== selected[0]) set(path, opt);
53
+ }
54
+ };
55
+
56
+ return (
57
+ <View style={styles.wrap}>
58
+ {label ? <Text style={[styles.label, { color: colors.text }]}>{label}</Text> : null}
59
+ <View
60
+ style={[
61
+ styles.group,
62
+ { backgroundColor: colors.border + "33" },
63
+ isVertical ? styles.groupVertical : styles.groupHorizontal,
64
+ ]}
65
+ >
66
+ {options.map((opt, index) => {
67
+ const isSelected = selected.includes(opt);
68
+ return (
69
+ <Pressable
70
+ key={index}
71
+ style={({ pressed }) => [
72
+ styles.option,
73
+ isSelected && { backgroundColor: accentHex },
74
+ pressed && styles.pressed,
75
+ !isVertical && styles.optionHorizontal,
76
+ ]}
77
+ onPress={() => handlePress(opt)}
78
+ >
79
+ <Text
80
+ style={[
81
+ styles.optionText,
82
+ { color: colors.text },
83
+ isSelected && styles.optionTextSelected,
84
+ ]}
85
+ >
86
+ {opt}
87
+ </Text>
88
+ </Pressable>
89
+ );
90
+ })}
91
+ </View>
92
+ </View>
93
+ );
94
+ }
95
+
96
+ const styles = StyleSheet.create({
97
+ wrap: { width: "100%", gap: 6 },
98
+ label: { fontSize: 13, fontWeight: "500" },
99
+ group: {
100
+ padding: 4,
101
+ borderRadius: 8,
102
+ gap: 4,
103
+ },
104
+ groupHorizontal: {
105
+ flexDirection: "row",
106
+ },
107
+ groupVertical: {
108
+ flexDirection: "column",
109
+ },
110
+ option: {
111
+ paddingVertical: 8,
112
+ paddingHorizontal: 12,
113
+ borderRadius: 6,
114
+ alignItems: "center",
115
+ justifyContent: "center",
116
+ },
117
+ optionHorizontal: {
118
+ flex: 1,
119
+ },
120
+ pressed: { opacity: 0.88 },
121
+ optionText: {
122
+ fontSize: 13,
123
+ fontWeight: "500",
124
+ },
125
+ optionTextSelected: {
126
+ color: "#fff",
127
+ },
128
+ });
@@ -0,0 +1,267 @@
1
+ import type { Spec } from "@json-render/core";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapCatalogView } from "./catalog-renderer";
4
+ import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "./theme";
5
+ import { hexToRgba } from "./use-snap-palette";
6
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
7
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
8
+ import {
9
+ DEFAULT_THEME_ACCENT,
10
+ PALETTE_LIGHT_HEX,
11
+ PALETTE_DARK_HEX,
12
+ type PaletteColor,
13
+ } from "@farcaster/snap";
14
+
15
+ // ─── Public types ──────────────────────────────────────
16
+
17
+ export type JsonValue =
18
+ | string
19
+ | number
20
+ | boolean
21
+ | null
22
+ | JsonValue[]
23
+ | { [key: string]: JsonValue };
24
+
25
+ export type SnapPage = {
26
+ version: string;
27
+ theme?: { accent?: string };
28
+ effects?: string[];
29
+ ui: Spec;
30
+ };
31
+
32
+ export type SnapActionHandlers = {
33
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
34
+ open_url: (target: string) => void;
35
+ open_mini_app: (target: string) => void;
36
+ view_cast: (params: { hash: string }) => void;
37
+ view_profile: (params: { fid: number }) => void;
38
+ compose_cast: (params: {
39
+ text?: string;
40
+ channelKey?: string;
41
+ embeds?: string[];
42
+ }) => void;
43
+ view_token: (params: { token: string }) => void;
44
+ send_token: (params: {
45
+ token: string;
46
+ amount?: string;
47
+ recipientFid?: number;
48
+ recipientAddress?: string;
49
+ }) => void;
50
+ swap_token: (params: {
51
+ sellToken?: string;
52
+ buyToken?: string;
53
+ }) => void;
54
+ };
55
+
56
+ // ─── Re-exports ───────────────────────────────────────
57
+
58
+ export { useSnapTheme, hexToRgba };
59
+ export type { SnapNativeColors };
60
+
61
+ // ─── Internal helpers ─────────────────────────────────
62
+
63
+ function applyStatePaths(
64
+ model: Record<string, unknown>,
65
+ changes: { path: string; value: unknown }[] | Record<string, unknown>,
66
+ ): void {
67
+ const entries = Array.isArray(changes)
68
+ ? changes.map((c) => [c.path, c.value] as const)
69
+ : Object.entries(changes);
70
+ for (const [path, value] of entries) {
71
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
72
+ const parts = trimmed.split("/").filter(Boolean);
73
+ if (parts.length < 2) continue;
74
+ const [top, ...rest] = parts;
75
+ if (top === "inputs") {
76
+ if (typeof model.inputs !== "object" || model.inputs === null) {
77
+ model.inputs = {};
78
+ }
79
+ const inputs = model.inputs as Record<string, unknown>;
80
+ if (rest.length === 1) {
81
+ inputs[rest[0]!] = value;
82
+ }
83
+ continue;
84
+ }
85
+ if (top === "theme") {
86
+ if (typeof model.theme !== "object" || model.theme === null) {
87
+ model.theme = {};
88
+ }
89
+ const theme = model.theme as Record<string, unknown>;
90
+ if (rest.length === 1) {
91
+ theme[rest[0]!] = value;
92
+ }
93
+ }
94
+ }
95
+ }
96
+
97
+ function resolveAccentHex(accent: string | undefined, appearance: "light" | "dark"): string {
98
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
99
+ const name = accent && Object.hasOwn(map, accent) ? (accent as PaletteColor) : DEFAULT_THEME_ACCENT;
100
+ return map[name];
101
+ }
102
+
103
+ // ─── SnapView ─────────────────────────────────────────
104
+
105
+ function SnapViewInner({
106
+ snap,
107
+ handlers,
108
+ loading = false,
109
+ }: {
110
+ snap: SnapPage;
111
+ handlers: SnapActionHandlers;
112
+ loading?: boolean;
113
+ }) {
114
+ const { mode } = useSnapTheme();
115
+ const spec = snap.ui;
116
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
117
+
118
+ const initialState = useMemo(
119
+ () => ({
120
+ ...(spec.state ?? {}),
121
+ inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
122
+ theme: {
123
+ ...((spec.state?.theme ?? {}) as Record<string, unknown>),
124
+ ...(snap.theme ? { accent: snap.theme.accent } : {}),
125
+ },
126
+ }),
127
+ [spec, snap.theme],
128
+ );
129
+
130
+ const stateRef = useRef<Record<string, unknown>>(initialState);
131
+
132
+ useEffect(() => {
133
+ stateRef.current = {
134
+ inputs: {
135
+ ...((initialState.inputs ?? {}) as Record<string, unknown>),
136
+ },
137
+ theme: {
138
+ ...((initialState.theme ?? {}) as Record<string, unknown>),
139
+ },
140
+ };
141
+ }, [initialState]);
142
+
143
+ useEffect(() => {
144
+ const result = snapJsonRenderCatalog.validate(spec);
145
+ if (!result.success) {
146
+ // eslint-disable-next-line no-console
147
+ console.warn("[SnapView] catalog validation issues:", result.error);
148
+ }
149
+ }, [spec]);
150
+
151
+ const [pageKey, setPageKey] = useState(0);
152
+ useEffect(() => {
153
+ setPageKey((k) => k + 1);
154
+ }, [spec]);
155
+
156
+ const handlersRef = useRef(handlers);
157
+ handlersRef.current = handlers;
158
+
159
+ const handleAction = useCallback((name: unknown, params: unknown) => {
160
+ const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
161
+ const p = (params ?? {}) as Record<string, unknown>;
162
+ const h = handlersRef.current;
163
+ switch (name) {
164
+ case "submit":
165
+ h.submit(String(p.target ?? ""), inputs);
166
+ break;
167
+ case "open_url":
168
+ h.open_url(String(p.target ?? ""));
169
+ break;
170
+ case "open_mini_app":
171
+ h.open_mini_app(String(p.target ?? ""));
172
+ break;
173
+ case "view_cast":
174
+ h.view_cast({ hash: String(p.hash ?? "") });
175
+ break;
176
+ case "view_profile":
177
+ h.view_profile({ fid: Number(p.fid ?? 0) });
178
+ break;
179
+ case "compose_cast":
180
+ h.compose_cast({
181
+ text: p.text ? String(p.text) : undefined,
182
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
183
+ embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
184
+ });
185
+ break;
186
+ case "view_token":
187
+ h.view_token({ token: String(p.token ?? "") });
188
+ break;
189
+ case "send_token":
190
+ h.send_token({
191
+ token: String(p.token ?? ""),
192
+ amount: p.amount ? String(p.amount) : undefined,
193
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
194
+ recipientAddress: p.recipientAddress ? String(p.recipientAddress) : undefined,
195
+ });
196
+ break;
197
+ case "swap_token":
198
+ h.swap_token({
199
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
200
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
201
+ });
202
+ break;
203
+ default:
204
+ break;
205
+ }
206
+ }, []);
207
+
208
+ return (
209
+ <View style={styles.container}>
210
+ {loading && (
211
+ <View
212
+ style={[
213
+ styles.overlay,
214
+ {
215
+ backgroundColor:
216
+ mode === "dark" ? "rgba(0,0,0,0.6)" : "rgba(255,255,255,0.75)",
217
+ },
218
+ ]}
219
+ >
220
+ <ActivityIndicator size="large" color={accentHex} />
221
+ </View>
222
+ )}
223
+ <SnapCatalogView
224
+ key={pageKey}
225
+ spec={spec}
226
+ state={initialState}
227
+ loading={false}
228
+ onStateChange={(changes) => {
229
+ applyStatePaths(stateRef.current, changes);
230
+ }}
231
+ onAction={handleAction}
232
+ />
233
+ </View>
234
+ );
235
+ }
236
+
237
+ export function SnapView({
238
+ snap,
239
+ handlers,
240
+ loading = false,
241
+ appearance = "dark",
242
+ colors,
243
+ }: {
244
+ snap: SnapPage;
245
+ handlers: SnapActionHandlers;
246
+ loading?: boolean;
247
+ appearance?: "light" | "dark";
248
+ colors?: Partial<SnapNativeColors>;
249
+ }) {
250
+ return (
251
+ <SnapThemeProvider appearance={appearance} colors={colors}>
252
+ <SnapViewInner snap={snap} handlers={handlers} loading={loading} />
253
+ </SnapThemeProvider>
254
+ );
255
+ }
256
+
257
+ const styles = StyleSheet.create({
258
+ container: {
259
+ width: "100%",
260
+ },
261
+ overlay: {
262
+ ...StyleSheet.absoluteFillObject,
263
+ alignItems: "center",
264
+ justifyContent: "center",
265
+ zIndex: 10,
266
+ },
267
+ });