@farcaster/snap 2.0.0 → 2.0.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 (194) hide show
  1. package/dist/colors.d.ts +4 -4
  2. package/dist/colors.js +20 -20
  3. package/dist/constants.d.ts +17 -1
  4. package/dist/constants.js +19 -1
  5. package/dist/index.d.ts +4 -6
  6. package/dist/index.js +2 -4
  7. package/dist/react/accent-context.d.ts +3 -1
  8. package/dist/react/accent-context.js +7 -4
  9. package/dist/react/catalog-renderer.js +4 -0
  10. package/dist/react/components/action-button.d.ts +2 -1
  11. package/dist/react/components/action-button.js +32 -13
  12. package/dist/react/components/badge.js +8 -8
  13. package/dist/react/components/bar-chart.d.ts +5 -0
  14. package/dist/react/components/bar-chart.js +26 -0
  15. package/dist/react/components/cell-grid.d.ts +5 -0
  16. package/dist/react/components/cell-grid.js +87 -0
  17. package/dist/react/components/icon.js +4 -10
  18. package/dist/react/components/input.js +12 -6
  19. package/dist/react/components/item-group.js +3 -1
  20. package/dist/react/components/item.d.ts +3 -3
  21. package/dist/react/components/item.js +4 -3
  22. package/dist/react/components/progress.js +3 -3
  23. package/dist/react/components/separator.js +3 -1
  24. package/dist/react/components/slider.js +15 -10
  25. package/dist/react/components/switch.js +10 -12
  26. package/dist/react/components/text.js +6 -14
  27. package/dist/react/components/toggle-group.js +20 -6
  28. package/dist/react/hooks/use-snap-colors.d.ts +38 -0
  29. package/dist/react/hooks/use-snap-colors.js +81 -0
  30. package/dist/react/index.d.ts +13 -1
  31. package/dist/react/index.js +9 -188
  32. package/dist/react/snap-view-core.d.ts +11 -0
  33. package/dist/react/snap-view-core.js +224 -0
  34. package/dist/react/v1/snap-view.d.ts +16 -0
  35. package/dist/react/v1/snap-view.js +90 -0
  36. package/dist/react/v2/snap-view.d.ts +23 -0
  37. package/dist/react/v2/snap-view.js +91 -0
  38. package/dist/react-native/catalog-renderer.d.ts +5 -0
  39. package/dist/react-native/catalog-renderer.js +40 -0
  40. package/dist/react-native/components/snap-action-button.d.ts +2 -0
  41. package/dist/react-native/components/snap-action-button.js +69 -0
  42. package/dist/react-native/components/snap-badge.d.ts +2 -0
  43. package/dist/react-native/components/snap-badge.js +41 -0
  44. package/dist/react-native/components/snap-bar-chart.d.ts +2 -0
  45. package/dist/react-native/components/snap-bar-chart.js +39 -0
  46. package/dist/react-native/components/snap-cell-grid.d.ts +2 -0
  47. package/dist/react-native/components/snap-cell-grid.js +94 -0
  48. package/dist/react-native/components/snap-icon.d.ts +5 -0
  49. package/dist/react-native/components/snap-icon.js +56 -0
  50. package/dist/react-native/components/snap-image.d.ts +2 -0
  51. package/dist/react-native/components/snap-image.js +23 -0
  52. package/dist/react-native/components/snap-input.d.ts +2 -0
  53. package/dist/react-native/components/snap-input.js +37 -0
  54. package/dist/react-native/components/snap-item-group.d.ts +5 -0
  55. package/dist/react-native/components/snap-item-group.js +23 -0
  56. package/dist/react-native/components/snap-item.d.ts +5 -0
  57. package/dist/react-native/components/snap-item.js +42 -0
  58. package/dist/react-native/components/snap-progress.d.ts +2 -0
  59. package/dist/react-native/components/snap-progress.js +26 -0
  60. package/dist/react-native/components/snap-separator.d.ts +2 -0
  61. package/dist/react-native/components/snap-separator.js +23 -0
  62. package/dist/react-native/components/snap-slider.d.ts +2 -0
  63. package/dist/react-native/components/snap-slider.js +43 -0
  64. package/dist/react-native/components/snap-stack.d.ts +5 -0
  65. package/dist/react-native/components/snap-stack.js +49 -0
  66. package/dist/react-native/components/snap-switch.d.ts +2 -0
  67. package/dist/react-native/components/snap-switch.js +31 -0
  68. package/dist/react-native/components/snap-text.d.ts +2 -0
  69. package/dist/react-native/components/snap-text.js +35 -0
  70. package/dist/react-native/components/snap-toggle-group.d.ts +2 -0
  71. package/dist/react-native/components/snap-toggle-group.js +99 -0
  72. package/dist/react-native/confetti-overlay.d.ts +1 -0
  73. package/dist/react-native/confetti-overlay.js +106 -0
  74. package/dist/react-native/index.d.ts +28 -0
  75. package/dist/react-native/index.js +15 -0
  76. package/dist/react-native/snap-view-core.d.ts +11 -0
  77. package/dist/react-native/snap-view-core.js +153 -0
  78. package/dist/react-native/theme.d.ts +27 -0
  79. package/dist/react-native/theme.js +43 -0
  80. package/dist/react-native/types.d.ts +42 -0
  81. package/dist/react-native/types.js +1 -0
  82. package/dist/react-native/use-snap-palette.d.ts +13 -0
  83. package/dist/react-native/use-snap-palette.js +48 -0
  84. package/dist/react-native/v1/snap-view.d.ts +24 -0
  85. package/dist/react-native/v1/snap-view.js +96 -0
  86. package/dist/react-native/v2/snap-view.d.ts +33 -0
  87. package/dist/react-native/v2/snap-view.js +114 -0
  88. package/dist/schemas.d.ts +100 -13
  89. package/dist/schemas.js +28 -10
  90. package/dist/server/parseRequest.d.ts +10 -0
  91. package/dist/server/parseRequest.js +48 -7
  92. package/dist/server/verify.d.ts +1 -0
  93. package/dist/server/verify.js +1 -0
  94. package/dist/ui/badge.d.ts +7 -2
  95. package/dist/ui/badge.js +2 -0
  96. package/dist/ui/bar-chart.d.ts +30 -0
  97. package/dist/ui/bar-chart.js +30 -0
  98. package/dist/ui/button.d.ts +4 -6
  99. package/dist/ui/button.js +1 -1
  100. package/dist/ui/catalog.d.ts +90 -16
  101. package/dist/ui/catalog.js +17 -3
  102. package/dist/ui/cell-grid.d.ts +34 -0
  103. package/dist/ui/cell-grid.js +39 -0
  104. package/dist/ui/icon.d.ts +2 -2
  105. package/dist/ui/image.d.ts +1 -2
  106. package/dist/ui/image.js +1 -1
  107. package/dist/ui/index.d.ts +4 -0
  108. package/dist/ui/index.js +2 -0
  109. package/dist/ui/item.d.ts +1 -3
  110. package/dist/ui/item.js +1 -1
  111. package/dist/ui/schema.d.ts +6 -2
  112. package/dist/ui/schema.js +2 -2
  113. package/dist/ui/slider.d.ts +1 -0
  114. package/dist/ui/slider.js +2 -0
  115. package/dist/ui/text.d.ts +2 -4
  116. package/dist/ui/text.js +2 -2
  117. package/dist/validator.d.ts +3 -2
  118. package/dist/validator.js +198 -2
  119. package/llms.txt +199 -0
  120. package/package.json +9 -3
  121. package/src/colors.ts +20 -20
  122. package/src/constants.ts +23 -1
  123. package/src/index.ts +16 -13
  124. package/src/react/accent-context.tsx +13 -6
  125. package/src/react/catalog-renderer.tsx +4 -0
  126. package/src/react/components/action-button.tsx +47 -20
  127. package/src/react/components/badge.tsx +14 -18
  128. package/src/react/components/bar-chart.tsx +69 -0
  129. package/src/react/components/cell-grid.tsx +128 -0
  130. package/src/react/components/icon.tsx +5 -18
  131. package/src/react/components/input.tsx +20 -9
  132. package/src/react/components/item-group.tsx +4 -1
  133. package/src/react/components/item.tsx +13 -10
  134. package/src/react/components/progress.tsx +12 -7
  135. package/src/react/components/separator.tsx +8 -1
  136. package/src/react/components/slider.tsx +28 -15
  137. package/src/react/components/switch.tsx +12 -16
  138. package/src/react/components/text.tsx +14 -23
  139. package/src/react/components/toggle-group.tsx +26 -9
  140. package/src/react/hooks/use-snap-colors.ts +128 -0
  141. package/src/react/index.tsx +49 -265
  142. package/src/react/snap-view-core.tsx +340 -0
  143. package/src/react/v1/snap-view.tsx +176 -0
  144. package/src/react/v2/snap-view.tsx +199 -0
  145. package/src/react-native/catalog-renderer.tsx +41 -0
  146. package/src/react-native/components/snap-action-button.tsx +96 -0
  147. package/src/react-native/components/snap-badge.tsx +60 -0
  148. package/src/react-native/components/snap-bar-chart.tsx +73 -0
  149. package/src/react-native/components/snap-cell-grid.tsx +150 -0
  150. package/src/react-native/components/snap-icon.tsx +102 -0
  151. package/src/react-native/components/snap-image.tsx +37 -0
  152. package/src/react-native/components/snap-input.tsx +58 -0
  153. package/src/react-native/components/snap-item-group.tsx +43 -0
  154. package/src/react-native/components/snap-item.tsx +66 -0
  155. package/src/react-native/components/snap-progress.tsx +40 -0
  156. package/src/react-native/components/snap-separator.tsx +32 -0
  157. package/src/react-native/components/snap-slider.tsx +85 -0
  158. package/src/react-native/components/snap-stack.tsx +66 -0
  159. package/src/react-native/components/snap-switch.tsx +46 -0
  160. package/src/react-native/components/snap-text.tsx +51 -0
  161. package/src/react-native/components/snap-toggle-group.tsx +127 -0
  162. package/src/react-native/confetti-overlay.tsx +134 -0
  163. package/src/react-native/index.tsx +83 -0
  164. package/src/react-native/snap-view-core.tsx +209 -0
  165. package/src/react-native/theme.tsx +85 -0
  166. package/src/react-native/types.ts +38 -0
  167. package/src/react-native/use-snap-palette.ts +64 -0
  168. package/src/react-native/v1/snap-view.tsx +229 -0
  169. package/src/react-native/v2/snap-view.tsx +283 -0
  170. package/src/schemas.ts +68 -17
  171. package/src/server/parseRequest.ts +68 -9
  172. package/src/server/verify.ts +2 -0
  173. package/src/ui/README.md +8 -8
  174. package/src/ui/badge.ts +2 -0
  175. package/src/ui/bar-chart.ts +38 -0
  176. package/src/ui/button.ts +1 -1
  177. package/src/ui/catalog.ts +19 -3
  178. package/src/ui/cell-grid.ts +49 -0
  179. package/src/ui/image.ts +1 -1
  180. package/src/ui/index.ts +6 -0
  181. package/src/ui/item.ts +1 -1
  182. package/src/ui/schema.ts +2 -2
  183. package/src/ui/slider.ts +2 -0
  184. package/src/ui/text.ts +2 -2
  185. package/src/validator.ts +246 -2
  186. package/dist/dataStore.d.ts +0 -12
  187. package/dist/dataStore.js +0 -35
  188. package/dist/middleware.d.ts +0 -3
  189. package/dist/middleware.js +0 -3
  190. package/dist/react/hooks/use-snap-accent.d.ts +0 -13
  191. package/dist/react/hooks/use-snap-accent.js +0 -32
  192. package/src/dataStore.ts +0 -62
  193. package/src/middleware.ts +0 -7
  194. package/src/react/hooks/use-snap-accent.ts +0 -45
@@ -0,0 +1,83 @@
1
+ import type { ReactNode } from "react";
2
+ import type { ValidationResult } from "@farcaster/snap";
3
+ import { SPEC_VERSION_2 } from "@farcaster/snap";
4
+ import type { SnapNativeColors } from "./theme";
5
+ import type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
6
+ import { useSnapTheme } from "./theme";
7
+ import { hexToRgba } from "./use-snap-palette";
8
+ import { SnapCardV1 } from "./v1/snap-view";
9
+ import { SnapCardV2 } from "./v2/snap-view";
10
+
11
+ // ─── Public types ──────────────────────────────────────
12
+
13
+ export type { JsonValue, SnapPage, SnapActionHandlers } from "./types";
14
+
15
+ // ─── Re-exports ───────────────────────────────────────
16
+
17
+ export { useSnapTheme, hexToRgba };
18
+ export type { SnapNativeColors };
19
+
20
+ // ─── SnapCard (version-switching) ─────────────────────
21
+
22
+ export function SnapCard({
23
+ snap,
24
+ handlers,
25
+ loading = false,
26
+ appearance = "dark",
27
+ colors,
28
+ borderRadius = 16,
29
+ showOverflowWarning = false,
30
+ onValidationError,
31
+ validationErrorFallback,
32
+ actionError,
33
+ plain = false,
34
+ }: {
35
+ snap: SnapPage;
36
+ handlers: SnapActionHandlers;
37
+ loading?: boolean;
38
+ appearance?: "light" | "dark";
39
+ colors?: Partial<SnapNativeColors>;
40
+ /** Border radius of the card (default 16). */
41
+ borderRadius?: number;
42
+ /** When true (v2 only), extends to 700px and shows a warning overlay below 500px. When false, clips at 500px. */
43
+ showOverflowWarning?: boolean;
44
+ /** Called when snap validation fails (v2 only). */
45
+ onValidationError?: (result: ValidationResult) => void;
46
+ /** Custom fallback rendered when validation fails (v2 only). */
47
+ validationErrorFallback?: ReactNode;
48
+ /** Server-side action error message to display inline. */
49
+ actionError?: string | null;
50
+ /** When true, renders without card frame (no border, background, or padding). */
51
+ plain?: boolean;
52
+ }) {
53
+ if (snap.version === SPEC_VERSION_2) {
54
+ return (
55
+ <SnapCardV2
56
+ snap={snap}
57
+ handlers={handlers}
58
+ loading={loading}
59
+ appearance={appearance}
60
+ colors={colors}
61
+ borderRadius={borderRadius}
62
+ showOverflowWarning={showOverflowWarning}
63
+ onValidationError={onValidationError}
64
+ validationErrorFallback={validationErrorFallback}
65
+ actionError={actionError}
66
+ plain={plain}
67
+ />
68
+ );
69
+ }
70
+
71
+ return (
72
+ <SnapCardV1
73
+ snap={snap}
74
+ handlers={handlers}
75
+ loading={loading}
76
+ appearance={appearance}
77
+ colors={colors}
78
+ borderRadius={borderRadius}
79
+ actionError={actionError}
80
+ plain={plain}
81
+ />
82
+ );
83
+ }
@@ -0,0 +1,209 @@
1
+ import type { Spec } from "@json-render/core";
2
+ import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
+ import { SnapCatalogView } from "./catalog-renderer";
4
+ import { useSnapTheme } from "./theme";
5
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
6
+ import { ActivityIndicator, StyleSheet, View } from "react-native";
7
+ import {
8
+ DEFAULT_THEME_ACCENT,
9
+ PALETTE_LIGHT_HEX,
10
+ PALETTE_DARK_HEX,
11
+ type PaletteColor,
12
+ } from "@farcaster/snap";
13
+ import type { SnapPage, SnapActionHandlers, JsonValue } from "./types";
14
+
15
+ // ─── Shared helpers ──────────────────────────────────
16
+
17
+ export function applyStatePaths(
18
+ model: Record<string, unknown>,
19
+ changes: { path: string; value: unknown }[] | Record<string, unknown>,
20
+ ): void {
21
+ const entries = Array.isArray(changes)
22
+ ? changes.map((c) => [c.path, c.value] as const)
23
+ : Object.entries(changes);
24
+ for (const [path, value] of entries) {
25
+ const trimmed = path.startsWith("/") ? path : `/${path}`;
26
+ const parts = trimmed.split("/").filter(Boolean);
27
+ if (parts.length < 2) continue;
28
+ const [top, ...rest] = parts;
29
+ if (top === "inputs") {
30
+ if (typeof model.inputs !== "object" || model.inputs === null) {
31
+ model.inputs = {};
32
+ }
33
+ const inputs = model.inputs as Record<string, unknown>;
34
+ if (rest.length === 1) {
35
+ inputs[rest[0]!] = value;
36
+ }
37
+ continue;
38
+ }
39
+ if (top === "theme") {
40
+ if (typeof model.theme !== "object" || model.theme === null) {
41
+ model.theme = {};
42
+ }
43
+ const theme = model.theme as Record<string, unknown>;
44
+ if (rest.length === 1) {
45
+ theme[rest[0]!] = value;
46
+ }
47
+ }
48
+ }
49
+ }
50
+
51
+ export function resolveAccentHex(
52
+ accent: string | undefined,
53
+ appearance: "light" | "dark",
54
+ ): string {
55
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
56
+ const name =
57
+ accent && Object.hasOwn(map, accent)
58
+ ? (accent as PaletteColor)
59
+ : DEFAULT_THEME_ACCENT;
60
+ return map[name];
61
+ }
62
+
63
+ // ─── Core rendering component (no validation) ────────
64
+
65
+ export function SnapViewCoreInner({
66
+ snap,
67
+ handlers,
68
+ loading = false,
69
+ }: {
70
+ snap: SnapPage;
71
+ handlers: SnapActionHandlers;
72
+ loading?: boolean;
73
+ }) {
74
+ const { mode } = useSnapTheme();
75
+ const spec = snap.ui;
76
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
77
+
78
+ const initialState = useMemo(
79
+ () => ({
80
+ ...(spec.state ?? {}),
81
+ inputs: { ...((spec.state?.inputs ?? {}) as Record<string, unknown>) },
82
+ theme: {
83
+ ...((spec.state?.theme ?? {}) as Record<string, unknown>),
84
+ ...(snap.theme ? { accent: snap.theme.accent } : {}),
85
+ },
86
+ }),
87
+ [spec, snap.theme],
88
+ );
89
+
90
+ const stateRef = useRef<Record<string, unknown>>(initialState);
91
+
92
+ useEffect(() => {
93
+ stateRef.current = {
94
+ inputs: {
95
+ ...((initialState.inputs ?? {}) as Record<string, unknown>),
96
+ },
97
+ theme: {
98
+ ...((initialState.theme ?? {}) as Record<string, unknown>),
99
+ },
100
+ };
101
+ }, [initialState]);
102
+
103
+ useEffect(() => {
104
+ const catalogResult = snapJsonRenderCatalog.validate(spec);
105
+ if (!catalogResult.success) {
106
+ // eslint-disable-next-line no-console
107
+ console.warn("[Snap] catalog validation issues:", catalogResult.error);
108
+ }
109
+ }, [spec]);
110
+
111
+ const [pageKey, setPageKey] = useState(0);
112
+ useEffect(() => {
113
+ setPageKey((k) => k + 1);
114
+ }, [spec]);
115
+
116
+ const handlersRef = useRef(handlers);
117
+ handlersRef.current = handlers;
118
+
119
+ const handleAction = useCallback((name: unknown, params: unknown) => {
120
+ const inputs = (stateRef.current.inputs ?? {}) as Record<string, JsonValue>;
121
+ const p = (params ?? {}) as Record<string, unknown>;
122
+ const h = handlersRef.current;
123
+ switch (name) {
124
+ case "submit":
125
+ h.submit(String(p.target ?? ""), inputs);
126
+ break;
127
+ case "open_url":
128
+ h.open_url(String(p.target ?? ""));
129
+ break;
130
+ case "open_mini_app":
131
+ h.open_mini_app(String(p.target ?? ""));
132
+ break;
133
+ case "view_cast":
134
+ h.view_cast({ hash: String(p.hash ?? "") });
135
+ break;
136
+ case "view_profile":
137
+ h.view_profile({ fid: Number(p.fid ?? 0) });
138
+ break;
139
+ case "compose_cast":
140
+ h.compose_cast({
141
+ text: p.text ? String(p.text) : undefined,
142
+ channelKey: p.channelKey ? String(p.channelKey) : undefined,
143
+ embeds: Array.isArray(p.embeds) ? (p.embeds as string[]) : undefined,
144
+ });
145
+ break;
146
+ case "view_token":
147
+ h.view_token({ token: String(p.token ?? "") });
148
+ break;
149
+ case "send_token":
150
+ h.send_token({
151
+ token: String(p.token ?? ""),
152
+ amount: p.amount ? String(p.amount) : undefined,
153
+ recipientFid: p.recipientFid ? Number(p.recipientFid) : undefined,
154
+ recipientAddress: p.recipientAddress
155
+ ? String(p.recipientAddress)
156
+ : undefined,
157
+ });
158
+ break;
159
+ case "swap_token":
160
+ h.swap_token({
161
+ sellToken: p.sellToken ? String(p.sellToken) : undefined,
162
+ buyToken: p.buyToken ? String(p.buyToken) : undefined,
163
+ });
164
+ break;
165
+ default:
166
+ break;
167
+ }
168
+ }, []);
169
+
170
+ return (
171
+ <View style={styles.container}>
172
+ {loading ? (
173
+ <View
174
+ style={[
175
+ styles.overlay,
176
+ {
177
+ backgroundColor:
178
+ mode === "dark" ? "rgba(0,0,0,0.1)" : "rgba(255,255,255,0.2)",
179
+ },
180
+ ]}
181
+ >
182
+ <ActivityIndicator size="large" color={accentHex} />
183
+ </View>
184
+ ) : null}
185
+ <SnapCatalogView
186
+ key={pageKey}
187
+ spec={spec}
188
+ state={initialState}
189
+ loading={false}
190
+ onStateChange={(changes) => {
191
+ applyStatePaths(stateRef.current, changes);
192
+ }}
193
+ onAction={handleAction}
194
+ />
195
+ </View>
196
+ );
197
+ }
198
+
199
+ const styles = StyleSheet.create({
200
+ container: {
201
+ width: "100%",
202
+ },
203
+ overlay: {
204
+ ...StyleSheet.absoluteFillObject,
205
+ alignItems: "center",
206
+ justifyContent: "center",
207
+ zIndex: 10,
208
+ },
209
+ });
@@ -0,0 +1,85 @@
1
+ import { createContext, useContext, useMemo, type ReactNode } from "react";
2
+
3
+ // ─── Color tokens ─────────────────────────────────────
4
+
5
+ export type SnapNativeColors = {
6
+ bg: string;
7
+ surface: string;
8
+ text: string;
9
+ textSecondary: string;
10
+ border: string;
11
+ inputBg: string;
12
+ muted: string;
13
+ /** Subtle tint for toggle button resting state */
14
+ mutedSubtle: string;
15
+ /** Slightly stronger tint for hover/press state */
16
+ mutedHover: string;
17
+ /** Stronger tint for selected state (toggle group) */
18
+ mutedSelected: string;
19
+ };
20
+
21
+ const DEFAULT_LIGHT: SnapNativeColors = {
22
+ bg: "#dfe3e8",
23
+ surface: "#ffffff",
24
+ text: "rgba(0,0,0,0.9)",
25
+ textSecondary: "rgba(0,0,0,0.5)",
26
+ border: "rgba(0,0,0,0.1)",
27
+ inputBg: "rgba(0,0,0,0.06)",
28
+ muted: "rgba(0,0,0,0.08)",
29
+ mutedSubtle: "rgba(0,0,0,0.04)",
30
+ mutedHover: "rgba(0,0,0,0.12)",
31
+ mutedSelected: "rgba(0,0,0,0.16)",
32
+ };
33
+
34
+ const DEFAULT_DARK: SnapNativeColors = {
35
+ bg: "#111318",
36
+ surface: "#1a1d24",
37
+ text: "rgba(255,255,255,0.9)",
38
+ textSecondary: "rgba(255,255,255,0.5)",
39
+ border: "rgba(255,255,255,0.1)",
40
+ inputBg: "rgba(255,255,255,0.04)",
41
+ muted: "rgba(255,255,255,0.06)",
42
+ mutedSubtle: "rgba(255,255,255,0.03)",
43
+ mutedHover: "rgba(255,255,255,0.08)",
44
+ mutedSelected: "rgba(255,255,255,0.12)",
45
+ };
46
+
47
+ // ─── Context ──────────────────────────────────────────
48
+
49
+ interface SnapThemeValue {
50
+ mode: "light" | "dark";
51
+ colors: SnapNativeColors;
52
+ }
53
+
54
+ const SnapThemeContext = createContext<SnapThemeValue>({
55
+ mode: "dark",
56
+ colors: DEFAULT_DARK,
57
+ });
58
+
59
+ export function SnapThemeProvider({
60
+ appearance,
61
+ colors,
62
+ children,
63
+ }: {
64
+ appearance: "light" | "dark";
65
+ colors?: Partial<SnapNativeColors>;
66
+ children: ReactNode;
67
+ }) {
68
+ const value = useMemo<SnapThemeValue>(() => {
69
+ const defaults = appearance === "dark" ? DEFAULT_DARK : DEFAULT_LIGHT;
70
+ return {
71
+ mode: appearance,
72
+ colors: colors ? { ...defaults, ...colors } : defaults,
73
+ };
74
+ }, [appearance, colors]);
75
+
76
+ return (
77
+ <SnapThemeContext.Provider value={value}>
78
+ {children}
79
+ </SnapThemeContext.Provider>
80
+ );
81
+ }
82
+
83
+ export function useSnapTheme(): SnapThemeValue {
84
+ return useContext(SnapThemeContext);
85
+ }
@@ -0,0 +1,38 @@
1
+ import type { Spec } from "@json-render/core";
2
+
3
+ export type JsonValue =
4
+ | string
5
+ | number
6
+ | boolean
7
+ | null
8
+ | JsonValue[]
9
+ | { [key: string]: JsonValue };
10
+
11
+ export type SnapPage = {
12
+ version: string;
13
+ theme?: { accent?: string };
14
+ effects?: string[];
15
+ ui: Spec;
16
+ };
17
+
18
+ export type SnapActionHandlers = {
19
+ submit: (target: string, inputs: Record<string, JsonValue>) => void;
20
+ open_url: (target: string) => void;
21
+ open_snap: (target: string) => void;
22
+ open_mini_app: (target: string) => void;
23
+ view_cast: (params: { hash: string }) => void;
24
+ view_profile: (params: { fid: number }) => void;
25
+ compose_cast: (params: {
26
+ text?: string;
27
+ channelKey?: string;
28
+ embeds?: string[];
29
+ }) => void;
30
+ view_token: (params: { token: string }) => void;
31
+ send_token: (params: {
32
+ token: string;
33
+ amount?: string;
34
+ recipientFid?: number;
35
+ recipientAddress?: string;
36
+ }) => void;
37
+ swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
38
+ };
@@ -0,0 +1,64 @@
1
+ import {
2
+ DEFAULT_THEME_ACCENT,
3
+ PALETTE_COLOR_VALUES,
4
+ PALETTE_LIGHT_HEX,
5
+ PALETTE_DARK_HEX,
6
+ type PaletteColor,
7
+ } from "@farcaster/snap";
8
+ import { useStateStore } from "@json-render/react-native";
9
+ import { useSnapTheme } from "./theme";
10
+
11
+ function resolveHex(name: string, appearance: "light" | "dark"): string {
12
+ const map = appearance === "dark" ? PALETTE_DARK_HEX : PALETTE_LIGHT_HEX;
13
+ if (Object.hasOwn(map, name)) {
14
+ return map[name as PaletteColor];
15
+ }
16
+ return map.purple;
17
+ }
18
+
19
+ function isPaletteColor(s: string): s is PaletteColor {
20
+ return (PALETTE_COLOR_VALUES as readonly string[]).includes(s);
21
+ }
22
+
23
+ function themeAccentFromStore(get: (path: string) => unknown): PaletteColor {
24
+ const raw = get("/theme/accent");
25
+ if (typeof raw === "string" && isPaletteColor(raw)) {
26
+ return raw;
27
+ }
28
+ return DEFAULT_THEME_ACCENT;
29
+ }
30
+
31
+ export function useSnapPalette() {
32
+ const { mode } = useSnapTheme();
33
+ const { get } = useStateStore();
34
+ const accentName = themeAccentFromStore(get);
35
+ const accentHex = resolveHex(accentName, mode);
36
+
37
+ const hex = (semantic: string) =>
38
+ semantic === "accent" ? accentHex : resolveHex(semantic, mode);
39
+
40
+ return { appearance: mode, accentName, accentHex, hex };
41
+ }
42
+
43
+ /** `#RRGGBB` + alpha → `rgba(...)` for React Native styles. */
44
+ export function hexToRgba(hex: string, alpha: number): string {
45
+ const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim());
46
+ if (!m) {
47
+ return `rgba(0,0,0,${alpha})`;
48
+ }
49
+ const n = Number.parseInt(m[1]!, 16);
50
+ const r = (n >> 16) & 255;
51
+ const g = (n >> 8) & 255;
52
+ const b = n & 255;
53
+ return `rgba(${r},${g},${b},${alpha})`;
54
+ }
55
+
56
+ export function useSnapPreviewChromePalette(themeAccent: string | undefined) {
57
+ const { mode } = useSnapTheme();
58
+ const accentName =
59
+ typeof themeAccent === "string" && isPaletteColor(themeAccent)
60
+ ? themeAccent
61
+ : DEFAULT_THEME_ACCENT;
62
+ const accentHex = resolveHex(accentName, mode);
63
+ return { appearance: mode, accentName, accentHex };
64
+ }
@@ -0,0 +1,229 @@
1
+ import { useEffect, useState } from "react";
2
+ import { View, Text, StyleSheet, Pressable } from "react-native";
3
+ import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
4
+ import { SnapViewCoreInner } from "../snap-view-core";
5
+ import type { SnapPage, SnapActionHandlers } from "../types";
6
+
7
+ const SNAP_MAX_HEIGHT = 500;
8
+
9
+ // ─── SnapViewV1 (no validation) ──────────────────────
10
+
11
+ export function SnapViewV1Inner({
12
+ snap,
13
+ handlers,
14
+ loading = false,
15
+ }: {
16
+ snap: SnapPage;
17
+ handlers: SnapActionHandlers;
18
+ loading?: boolean;
19
+ }) {
20
+ return (
21
+ <SnapViewCoreInner snap={snap} handlers={handlers} loading={loading} />
22
+ );
23
+ }
24
+
25
+ export function SnapViewV1({
26
+ snap,
27
+ handlers,
28
+ loading = false,
29
+ appearance = "dark",
30
+ colors,
31
+ }: {
32
+ snap: SnapPage;
33
+ handlers: SnapActionHandlers;
34
+ loading?: boolean;
35
+ appearance?: "light" | "dark";
36
+ colors?: Partial<SnapNativeColors>;
37
+ }) {
38
+ return (
39
+ <SnapThemeProvider appearance={appearance} colors={colors}>
40
+ <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
41
+ </SnapThemeProvider>
42
+ );
43
+ }
44
+
45
+ // ─── SnapCardV1 (card frame with expandable clipping) ──
46
+
47
+ function SnapCardV1Inner({
48
+ snap,
49
+ handlers,
50
+ loading = false,
51
+ borderRadius,
52
+ actionError,
53
+ appearance,
54
+ plain,
55
+ }: {
56
+ snap: SnapPage;
57
+ handlers: SnapActionHandlers;
58
+ loading?: boolean;
59
+ borderRadius: number;
60
+ actionError?: string | null;
61
+ appearance: "light" | "dark";
62
+ plain: boolean;
63
+ }) {
64
+ const { colors } = useSnapTheme();
65
+ const [contentHeight, setContentHeight] = useState(0);
66
+ const [isExpanded, setIsExpanded] = useState(false);
67
+
68
+ useEffect(() => {
69
+ setIsExpanded(false);
70
+ setContentHeight(0);
71
+ }, [snap]);
72
+
73
+ const isExpandable = contentHeight > SNAP_MAX_HEIGHT + 1;
74
+ const isClipped = isExpandable && !isExpanded;
75
+
76
+ return (
77
+ <>
78
+ <View style={cardStyles.frameRing}>
79
+ <View
80
+ style={[
81
+ plain ? undefined : cardStyles.card,
82
+ plain ? undefined : {
83
+ borderRadius,
84
+ borderColor: colors.border,
85
+ backgroundColor: colors.surface,
86
+ },
87
+ ]}
88
+ >
89
+ <View
90
+ style={isClipped ? { maxHeight: SNAP_MAX_HEIGHT, overflow: "hidden" } : undefined}
91
+ >
92
+ <View
93
+ collapsable={false}
94
+ onLayout={(event) => {
95
+ const nextHeight = Math.round(event.nativeEvent.layout.height);
96
+ setContentHeight((currentHeight) =>
97
+ isClipped
98
+ ? Math.max(currentHeight, nextHeight)
99
+ : currentHeight === nextHeight
100
+ ? currentHeight
101
+ : nextHeight,
102
+ );
103
+ }}
104
+ style={plain ? undefined : cardStyles.body}
105
+ >
106
+ <SnapViewV1Inner
107
+ snap={snap}
108
+ handlers={handlers}
109
+ loading={loading}
110
+ />
111
+ </View>
112
+ </View>
113
+ {isExpandable ? (
114
+ <View
115
+ style={[
116
+ cardStyles.expandRow,
117
+ plain
118
+ ? cardStyles.expandRowPlain
119
+ : { borderTopColor: colors.border },
120
+ ]}
121
+ >
122
+ <Pressable
123
+ style={({ pressed }) => [
124
+ cardStyles.expandButton,
125
+ {
126
+ backgroundColor: pressed
127
+ ? colors.mutedHover
128
+ : colors.muted,
129
+ },
130
+ ]}
131
+ onPress={() => {
132
+ setIsExpanded((value) => !value);
133
+ }}
134
+ >
135
+ <Text
136
+ style={[cardStyles.expandButtonText, { color: colors.text }]}
137
+ >
138
+ {isExpanded ? "Show less" : "Show more"}
139
+ </Text>
140
+ </Pressable>
141
+ </View>
142
+ ) : null}
143
+ </View>
144
+ </View>
145
+ {actionError && (
146
+ <Text
147
+ style={[
148
+ cardStyles.actionError,
149
+ {
150
+ color:
151
+ appearance === "dark"
152
+ ? "rgba(255,100,100,0.9)"
153
+ : "rgba(200,0,0,0.8)",
154
+ },
155
+ ]}
156
+ >
157
+ {actionError}
158
+ </Text>
159
+ )}
160
+ </>
161
+ );
162
+ }
163
+
164
+ export function SnapCardV1({
165
+ snap,
166
+ handlers,
167
+ loading = false,
168
+ appearance = "dark",
169
+ colors,
170
+ borderRadius = 16,
171
+ actionError,
172
+ plain = false,
173
+ }: {
174
+ snap: SnapPage;
175
+ handlers: SnapActionHandlers;
176
+ loading?: boolean;
177
+ appearance?: "light" | "dark";
178
+ colors?: Partial<SnapNativeColors>;
179
+ borderRadius?: number;
180
+ actionError?: string | null;
181
+ plain?: boolean;
182
+ }) {
183
+ return (
184
+ <SnapThemeProvider appearance={appearance} colors={colors}>
185
+ <SnapCardV1Inner
186
+ snap={snap}
187
+ handlers={handlers}
188
+ loading={loading}
189
+ borderRadius={borderRadius}
190
+ actionError={actionError}
191
+ appearance={appearance}
192
+ plain={plain}
193
+ />
194
+ </SnapThemeProvider>
195
+ );
196
+ }
197
+
198
+ const cardStyles = StyleSheet.create({
199
+ frameRing: { alignSelf: "stretch" },
200
+ card: { overflow: "hidden", borderWidth: 1, minHeight: 120 },
201
+ body: { paddingHorizontal: 16, paddingVertical: 16 },
202
+ expandRow: {
203
+ alignItems: "center",
204
+ paddingHorizontal: 16,
205
+ paddingTop: 10,
206
+ paddingBottom: 12,
207
+ borderTopWidth: StyleSheet.hairlineWidth,
208
+ },
209
+ expandRowPlain: {
210
+ paddingHorizontal: 0,
211
+ paddingTop: 8,
212
+ paddingBottom: 0,
213
+ borderTopWidth: 0,
214
+ },
215
+ expandButton: {
216
+ minWidth: 92,
217
+ alignItems: "center",
218
+ justifyContent: "center",
219
+ borderRadius: 9999,
220
+ paddingHorizontal: 10,
221
+ paddingVertical: 6,
222
+ },
223
+ expandButtonText: {
224
+ fontSize: 13,
225
+ lineHeight: 18,
226
+ fontWeight: "600",
227
+ },
228
+ actionError: { paddingHorizontal: 12, paddingVertical: 8, fontSize: 13 },
229
+ });