@farcaster/snap 2.0.1 → 2.0.3

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/validator.js CHANGED
@@ -3,7 +3,12 @@ import { MAX_CHILDREN, MAX_DEPTH, MAX_ELEMENTS, MAX_ROOT_CHILDREN, SPEC_VERSION_
3
3
  import { snapJsonRenderCatalog } from "./ui/catalog.js";
4
4
  // ─── Helpers ──────────────────────────────────────────
5
5
  /** Actions whose `params.target` must be a valid URL. */
6
- const URL_TARGET_ACTIONS = new Set(["submit", "open_url", "open_mini_app"]);
6
+ const URL_TARGET_ACTIONS = new Set([
7
+ "submit",
8
+ "open_url",
9
+ "open_snap",
10
+ "open_mini_app",
11
+ ]);
7
12
  /** Image file extensions allowed in image URLs. */
8
13
  const ALLOWED_IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "gif", "webp"]);
9
14
  /**
@@ -129,7 +134,7 @@ function validateStructure(ui) {
129
134
  /**
130
135
  * Validate all URLs in the snap:
131
136
  * - image.url: must be HTTPS with allowed extension
132
- * - action target URLs (submit, open_url, open_mini_app): must be HTTPS
137
+ * - action target URLs (submit, open_url, open_snap, open_mini_app): must be HTTPS
133
138
  */
134
139
  function validateUrls(elements) {
135
140
  const issues = [];
package/llms.txt CHANGED
@@ -68,7 +68,7 @@ Top-level fields: `version` (required, `"1.0"` or `"2.0"`), `theme` (optional, `
68
68
  - `title` (string, required, max 100)
69
69
  - `description` (string, optional, max 160)
70
70
  - `variant` (optional): `"default"`. Default: `"default"`
71
- - Children render in the actions slot (right side)
71
+ - Children render in the actions slot (right side). Badges, buttons, and icons are all valid — but the item itself is not interactive, so avoid navigation-style icons (`chevron-right`, `arrow-right`, `external-link`) that imply the row navigates.
72
72
 
73
73
  **progress** — Horizontal progress bar.
74
74
  - `value` (number, required, 0 to max)
@@ -165,7 +165,7 @@ Bound to buttons via `on.press`:
165
165
  | `open_mini_app` | `target` (URL) | Open as Farcaster mini app |
166
166
  | `view_cast` | `hash` (string) | Navigate to a cast |
167
167
  | `view_profile` | `fid` (number) | Navigate to a profile |
168
- | `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer |
168
+ | `compose_cast` | `text?`, `channelKey?`, `embeds?` | Open cast composer. Put URLs in `embeds`, not `text` |
169
169
  | `view_token` | `token` (CAIP-19) | View token in wallet |
170
170
  | `send_token` | `token`, `amount?`, `recipientFid?`, `recipientAddress?` | Send token flow |
171
171
  | `swap_token` | `sellToken?`, `buyToken?` | Swap token flow |
@@ -174,6 +174,8 @@ Bound to buttons via `on.press`:
174
174
 
175
175
  `arrow-right`, `arrow-left`, `external-link`, `chevron-right`, `check`, `x`, `alert-triangle`, `info`, `clock`, `heart`, `message-circle`, `repeat`, `share`, `user`, `users`, `star`, `trophy`, `zap`, `flame`, `gift`, `image`, `play`, `pause`, `wallet`, `coins`, `plus`, `minus`, `refresh-cw`, `bookmark`, `thumbs-up`, `thumbs-down`, `trending-up`, `trending-down`
176
176
 
177
+ `chevron-right`, `arrow-right`, and `external-link` are navigation/disclosure affordances — only use them when the surrounding element actually navigates (e.g. a button bound to `open_url` or `open_snap`). Never place them inside an `item`'s actions slot; `item` is not interactive.
178
+
177
179
  ## Color Palette
178
180
 
179
181
  `gray`, `blue`, `red`, `amber`, `green`, `teal`, `purple`, `pink`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farcaster/snap",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Farcaster Snaps 🫰",
5
5
  "repository": {
6
6
  "type": "git",
@@ -39,19 +39,22 @@ export function SnapActionButton({
39
39
  const Icon = iconName ? ICON_MAP[iconName] : undefined;
40
40
  const showExternalIcon = isExternalLinkAction(element.on);
41
41
 
42
- const style = isPrimary
43
- ? {
44
- backgroundColor: hovered ? colors.accentHover : colors.accent,
45
- color: colors.accentFg,
46
- borderColor: "transparent",
47
- }
48
- : {
49
- backgroundColor: hovered
50
- ? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
51
- : colors.muted,
52
- color: colors.text,
53
- borderColor: "transparent",
54
- };
42
+ const style = {
43
+ cursor: "pointer" as const,
44
+ ...(isPrimary
45
+ ? {
46
+ backgroundColor: hovered ? colors.accentHover : colors.accent,
47
+ color: colors.accentFg,
48
+ borderColor: "transparent",
49
+ }
50
+ : {
51
+ backgroundColor: hovered
52
+ ? `color-mix(in srgb, ${colors.accent} 15%, transparent)`
53
+ : colors.muted,
54
+ color: colors.text,
55
+ borderColor: "transparent",
56
+ }),
57
+ };
55
58
 
56
59
  return (
57
60
  <div className="w-full min-w-0 flex-1">
@@ -59,6 +59,7 @@ export function SnapCard({
59
59
  validationErrorFallback,
60
60
  actionError,
61
61
  plain = false,
62
+ loadingOverlay,
62
63
  }: {
63
64
  snap: SnapPage;
64
65
  handlers: SnapActionHandlers;
@@ -73,6 +74,8 @@ export function SnapCard({
73
74
  actionError?: string | null;
74
75
  /** When true, renders without card frame (no border, background, or padding). */
75
76
  plain?: boolean;
77
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
78
+ loadingOverlay?: ReactNode;
76
79
  }) {
77
80
  if (snap.version === SPEC_VERSION_2) {
78
81
  return (
@@ -87,6 +90,7 @@ export function SnapCard({
87
90
  validationErrorFallback={validationErrorFallback}
88
91
  actionError={actionError}
89
92
  plain={plain}
93
+ loadingOverlay={loadingOverlay}
90
94
  />
91
95
  );
92
96
  }
@@ -100,6 +104,7 @@ export function SnapCard({
100
104
  maxWidth={maxWidth}
101
105
  actionError={actionError}
102
106
  plain={plain}
107
+ loadingOverlay={loadingOverlay}
103
108
  />
104
109
  );
105
110
  }
@@ -8,6 +8,7 @@ import { resolveSnapPaletteHex } from "./lib/resolve-palette-hex";
8
8
  import { snapPreviewPrimaryCssProperties } from "./lib/preview-primary-css";
9
9
  import {
10
10
  type CSSProperties,
11
+ type ReactNode,
11
12
  useCallback,
12
13
  useEffect,
13
14
  useMemo,
@@ -111,7 +112,7 @@ function ConfettiOverlay() {
111
112
  );
112
113
  }
113
114
 
114
- function SnapLoadingOverlay({
115
+ export function SnapLoadingOverlay({
115
116
  appearance,
116
117
  accentHex,
117
118
  active,
@@ -197,11 +198,17 @@ export function SnapViewCore({
197
198
  handlers,
198
199
  loading = false,
199
200
  appearance = "dark",
201
+ loadingOverlay,
200
202
  }: {
201
203
  snap: SnapPage;
202
204
  handlers: SnapActionHandlers;
203
205
  loading?: boolean;
204
206
  appearance?: "light" | "dark";
207
+ /**
208
+ * Custom content rendered while `loading` is true. When `undefined` (default)
209
+ * the built-in spinner + backdrop is used. Pass `null` to render nothing.
210
+ */
211
+ loadingOverlay?: ReactNode;
205
212
  }) {
206
213
  const spec = snap.ui;
207
214
  const initialState = useMemo(() => spec.state ?? { inputs: {} }, [spec]);
@@ -265,6 +272,9 @@ export function SnapViewCore({
265
272
  case "open_url":
266
273
  handlers.open_url(String(p.target ?? ""));
267
274
  break;
275
+ case "open_snap":
276
+ handlers.open_snap(String(p.target ?? ""));
277
+ break;
268
278
  case "open_mini_app":
269
279
  handlers.open_mini_app(String(p.target ?? ""));
270
280
  break;
@@ -312,11 +322,15 @@ export function SnapViewCore({
312
322
  return (
313
323
  <div style={{ position: "relative", width: "100%" }}>
314
324
  {showConfetti && <ConfettiOverlay />}
315
- <SnapLoadingOverlay
316
- appearance={appearance}
317
- accentHex={accentHex}
318
- active={loading}
319
- />
325
+ {loadingOverlay === undefined ? (
326
+ <SnapLoadingOverlay
327
+ appearance={appearance}
328
+ accentHex={accentHex}
329
+ active={loading}
330
+ />
331
+ ) : loading ? (
332
+ <>{loadingOverlay}</>
333
+ ) : null}
320
334
 
321
335
  <div style={previewSurfaceStyle}>
322
336
  <SnapPreviewAccentProvider
@@ -1,7 +1,8 @@
1
1
  "use client";
2
2
 
3
- import { useEffect, useRef, useState } from "react";
4
- import { SnapViewCore } from "../snap-view-core";
3
+ import { type ReactNode, useEffect, useMemo, useRef, useState } from "react";
4
+ import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
5
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
5
6
  import type { SnapPage, SnapActionHandlers } from "../index";
6
7
 
7
8
  const SNAP_MAX_HEIGHT = 500;
@@ -11,11 +12,14 @@ export function SnapViewV1({
11
12
  handlers,
12
13
  loading = false,
13
14
  appearance = "dark",
15
+ loadingOverlay,
14
16
  }: {
15
17
  snap: SnapPage;
16
18
  handlers: SnapActionHandlers;
17
19
  loading?: boolean;
18
20
  appearance?: "light" | "dark";
21
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
22
+ loadingOverlay?: ReactNode;
19
23
  }) {
20
24
  return (
21
25
  <SnapViewCore
@@ -23,6 +27,7 @@ export function SnapViewV1({
23
27
  handlers={handlers}
24
28
  loading={loading}
25
29
  appearance={appearance}
30
+ loadingOverlay={loadingOverlay}
26
31
  />
27
32
  );
28
33
  }
@@ -35,6 +40,7 @@ export function SnapCardV1({
35
40
  maxWidth = 480,
36
41
  actionError,
37
42
  plain = false,
43
+ loadingOverlay,
38
44
  }: {
39
45
  snap: SnapPage;
40
46
  handlers: SnapActionHandlers;
@@ -43,6 +49,8 @@ export function SnapCardV1({
43
49
  maxWidth?: number;
44
50
  actionError?: string | null;
45
51
  plain?: boolean;
52
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
53
+ loadingOverlay?: ReactNode;
46
54
  }) {
47
55
  const isDark = appearance === "dark";
48
56
  const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
@@ -86,6 +94,11 @@ export function SnapCardV1({
86
94
 
87
95
  const isClipped = isExpandable && !isExpanded;
88
96
 
97
+ const accentHex = useMemo(
98
+ () => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
99
+ [snap.theme?.accent, appearance],
100
+ );
101
+
89
102
  return (
90
103
  <div
91
104
  style={{
@@ -116,9 +129,19 @@ export function SnapCardV1({
116
129
  handlers={handlers}
117
130
  loading={loading}
118
131
  appearance={appearance}
132
+ loadingOverlay={null}
119
133
  />
120
134
  </div>
121
135
  </div>
136
+ {loadingOverlay === undefined ? (
137
+ <SnapLoadingOverlay
138
+ appearance={appearance}
139
+ accentHex={accentHex}
140
+ active={loading}
141
+ />
142
+ ) : loading ? (
143
+ <>{loadingOverlay}</>
144
+ ) : null}
122
145
  {isExpandable ? (
123
146
  <div
124
147
  style={{
@@ -3,7 +3,8 @@
3
3
  import { type ReactNode, useEffect, useMemo } from "react";
4
4
  import { validateSnapResponse } from "../../validator.js";
5
5
  import type { ValidationResult } from "../../validator.js";
6
- import { SnapViewCore } from "../snap-view-core";
6
+ import { SnapViewCore, SnapLoadingOverlay } from "../snap-view-core";
7
+ import { resolveSnapPaletteHex } from "../lib/resolve-palette-hex";
7
8
  import type { SnapPage, SnapActionHandlers } from "../index";
8
9
 
9
10
  const SNAP_MAX_HEIGHT = 500;
@@ -45,6 +46,7 @@ export function SnapViewV2({
45
46
  appearance = "dark",
46
47
  onValidationError,
47
48
  validationErrorFallback,
49
+ loadingOverlay,
48
50
  }: {
49
51
  snap: SnapPage;
50
52
  handlers: SnapActionHandlers;
@@ -52,6 +54,8 @@ export function SnapViewV2({
52
54
  appearance?: "light" | "dark";
53
55
  onValidationError?: (result: ValidationResult) => void;
54
56
  validationErrorFallback?: ReactNode;
57
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
58
+ loadingOverlay?: ReactNode;
55
59
  }) {
56
60
  const validation = useMemo(() => validateSnapResponse(snap), [snap]);
57
61
  const valid = validation.valid;
@@ -79,6 +83,7 @@ export function SnapViewV2({
79
83
  handlers={handlers}
80
84
  loading={loading}
81
85
  appearance={appearance}
86
+ loadingOverlay={loadingOverlay}
82
87
  />
83
88
  );
84
89
  }
@@ -96,6 +101,7 @@ export function SnapCardV2({
96
101
  validationErrorFallback,
97
102
  actionError,
98
103
  plain = false,
104
+ loadingOverlay,
99
105
  }: {
100
106
  snap: SnapPage;
101
107
  handlers: SnapActionHandlers;
@@ -107,12 +113,18 @@ export function SnapCardV2({
107
113
  validationErrorFallback?: ReactNode;
108
114
  actionError?: string | null;
109
115
  plain?: boolean;
116
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
117
+ loadingOverlay?: ReactNode;
110
118
  }) {
111
119
  const maxHeight = showOverflowWarning ? SNAP_WARNING_HEIGHT : SNAP_MAX_HEIGHT;
112
120
  const isDark = appearance === "dark";
113
121
  const bg = isDark ? "rgba(0,0,0,0.85)" : "rgba(255,255,255,0.9)";
114
122
  const borderColor = isDark ? "rgba(255,255,255,0.1)" : "rgba(0,0,0,0.1)";
115
123
  const surfaceBg = isDark ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.02)";
124
+ const accentHex = useMemo(
125
+ () => resolveSnapPaletteHex(snap.theme?.accent ?? "purple", appearance),
126
+ [snap.theme?.accent, appearance],
127
+ );
116
128
 
117
129
  return (
118
130
  <>
@@ -138,8 +150,18 @@ export function SnapCardV2({
138
150
  appearance={appearance}
139
151
  onValidationError={onValidationError}
140
152
  validationErrorFallback={validationErrorFallback}
153
+ loadingOverlay={null}
141
154
  />
142
155
  </div>
156
+ {loadingOverlay === undefined ? (
157
+ <SnapLoadingOverlay
158
+ appearance={appearance}
159
+ accentHex={accentHex}
160
+ active={loading}
161
+ />
162
+ ) : loading ? (
163
+ <>{loadingOverlay}</>
164
+ ) : null}
143
165
  {showOverflowWarning && (
144
166
  <div
145
167
  style={{
@@ -31,6 +31,7 @@ export function SnapCard({
31
31
  validationErrorFallback,
32
32
  actionError,
33
33
  plain = false,
34
+ loadingOverlay,
34
35
  }: {
35
36
  snap: SnapPage;
36
37
  handlers: SnapActionHandlers;
@@ -49,6 +50,8 @@ export function SnapCard({
49
50
  actionError?: string | null;
50
51
  /** When true, renders without card frame (no border, background, or padding). */
51
52
  plain?: boolean;
53
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
54
+ loadingOverlay?: ReactNode;
52
55
  }) {
53
56
  if (snap.version === SPEC_VERSION_2) {
54
57
  return (
@@ -64,6 +67,7 @@ export function SnapCard({
64
67
  validationErrorFallback={validationErrorFallback}
65
68
  actionError={actionError}
66
69
  plain={plain}
70
+ loadingOverlay={loadingOverlay}
67
71
  />
68
72
  );
69
73
  }
@@ -78,6 +82,7 @@ export function SnapCard({
78
82
  borderRadius={borderRadius}
79
83
  actionError={actionError}
80
84
  plain={plain}
85
+ loadingOverlay={loadingOverlay}
81
86
  />
82
87
  );
83
88
  }
@@ -2,7 +2,14 @@ import type { Spec } from "@json-render/core";
2
2
  import { snapJsonRenderCatalog } from "@farcaster/snap/ui";
3
3
  import { SnapCatalogView } from "./catalog-renderer";
4
4
  import { useSnapTheme } from "./theme";
5
- import { useCallback, useEffect, useMemo, useRef, useState } from "react";
5
+ import {
6
+ type ReactNode,
7
+ useCallback,
8
+ useEffect,
9
+ useMemo,
10
+ useRef,
11
+ useState,
12
+ } from "react";
6
13
  import { ActivityIndicator, StyleSheet, View } from "react-native";
7
14
  import {
8
15
  DEFAULT_THEME_ACCENT,
@@ -66,10 +73,16 @@ export function SnapViewCoreInner({
66
73
  snap,
67
74
  handlers,
68
75
  loading = false,
76
+ loadingOverlay,
69
77
  }: {
70
78
  snap: SnapPage;
71
79
  handlers: SnapActionHandlers;
72
80
  loading?: boolean;
81
+ /**
82
+ * Custom content rendered while `loading` is true. When `undefined` (default)
83
+ * the built-in ActivityIndicator overlay is used. Pass `null` to render nothing.
84
+ */
85
+ loadingOverlay?: ReactNode;
73
86
  }) {
74
87
  const { mode } = useSnapTheme();
75
88
  const spec = snap.ui;
@@ -127,6 +140,9 @@ export function SnapViewCoreInner({
127
140
  case "open_url":
128
141
  h.open_url(String(p.target ?? ""));
129
142
  break;
143
+ case "open_snap":
144
+ h.open_snap(String(p.target ?? ""));
145
+ break;
130
146
  case "open_mini_app":
131
147
  h.open_mini_app(String(p.target ?? ""));
132
148
  break;
@@ -169,19 +185,16 @@ export function SnapViewCoreInner({
169
185
 
170
186
  return (
171
187
  <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}
188
+ {loading
189
+ ? loadingOverlay === undefined
190
+ ? (
191
+ <SnapLoadingOverlay
192
+ appearance={mode}
193
+ accentHex={accentHex}
194
+ />
195
+ )
196
+ : loadingOverlay
197
+ : null}
185
198
  <SnapCatalogView
186
199
  key={pageKey}
187
200
  spec={spec}
@@ -196,6 +209,30 @@ export function SnapViewCoreInner({
196
209
  );
197
210
  }
198
211
 
212
+ export function SnapLoadingOverlay({
213
+ appearance,
214
+ accentHex,
215
+ }: {
216
+ appearance: "light" | "dark";
217
+ accentHex: string;
218
+ }) {
219
+ return (
220
+ <View
221
+ style={[
222
+ styles.overlay,
223
+ {
224
+ backgroundColor:
225
+ appearance === "dark"
226
+ ? "rgba(0,0,0,0.1)"
227
+ : "rgba(255,255,255,0.2)",
228
+ },
229
+ ]}
230
+ >
231
+ <ActivityIndicator size="large" color={accentHex} />
232
+ </View>
233
+ );
234
+ }
235
+
199
236
  const styles = StyleSheet.create({
200
237
  container: {
201
238
  width: "100%",
@@ -1,7 +1,11 @@
1
- import { useEffect, useState } from "react";
1
+ import { type ReactNode, useEffect, useState } from "react";
2
2
  import { View, Text, StyleSheet, Pressable } from "react-native";
3
3
  import { SnapThemeProvider, useSnapTheme, type SnapNativeColors } from "../theme";
4
- import { SnapViewCoreInner } from "../snap-view-core";
4
+ import {
5
+ SnapLoadingOverlay,
6
+ SnapViewCoreInner,
7
+ resolveAccentHex,
8
+ } from "../snap-view-core";
5
9
  import type { SnapPage, SnapActionHandlers } from "../types";
6
10
 
7
11
  const SNAP_MAX_HEIGHT = 500;
@@ -12,13 +16,20 @@ export function SnapViewV1Inner({
12
16
  snap,
13
17
  handlers,
14
18
  loading = false,
19
+ loadingOverlay,
15
20
  }: {
16
21
  snap: SnapPage;
17
22
  handlers: SnapActionHandlers;
18
23
  loading?: boolean;
24
+ loadingOverlay?: ReactNode;
19
25
  }) {
20
26
  return (
21
- <SnapViewCoreInner snap={snap} handlers={handlers} loading={loading} />
27
+ <SnapViewCoreInner
28
+ snap={snap}
29
+ handlers={handlers}
30
+ loading={loading}
31
+ loadingOverlay={loadingOverlay}
32
+ />
22
33
  );
23
34
  }
24
35
 
@@ -28,16 +39,24 @@ export function SnapViewV1({
28
39
  loading = false,
29
40
  appearance = "dark",
30
41
  colors,
42
+ loadingOverlay,
31
43
  }: {
32
44
  snap: SnapPage;
33
45
  handlers: SnapActionHandlers;
34
46
  loading?: boolean;
35
47
  appearance?: "light" | "dark";
36
48
  colors?: Partial<SnapNativeColors>;
49
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
50
+ loadingOverlay?: ReactNode;
37
51
  }) {
38
52
  return (
39
53
  <SnapThemeProvider appearance={appearance} colors={colors}>
40
- <SnapViewV1Inner snap={snap} handlers={handlers} loading={loading} />
54
+ <SnapViewV1Inner
55
+ snap={snap}
56
+ handlers={handlers}
57
+ loading={loading}
58
+ loadingOverlay={loadingOverlay}
59
+ />
41
60
  </SnapThemeProvider>
42
61
  );
43
62
  }
@@ -52,6 +71,7 @@ function SnapCardV1Inner({
52
71
  actionError,
53
72
  appearance,
54
73
  plain,
74
+ loadingOverlay,
55
75
  }: {
56
76
  snap: SnapPage;
57
77
  handlers: SnapActionHandlers;
@@ -60,8 +80,10 @@ function SnapCardV1Inner({
60
80
  actionError?: string | null;
61
81
  appearance: "light" | "dark";
62
82
  plain: boolean;
83
+ loadingOverlay?: ReactNode;
63
84
  }) {
64
- const { colors } = useSnapTheme();
85
+ const { colors, mode } = useSnapTheme();
86
+ const accentHex = resolveAccentHex(snap.theme?.accent, mode);
65
87
  const [contentHeight, setContentHeight] = useState(0);
66
88
  const [isExpanded, setIsExpanded] = useState(false);
67
89
 
@@ -107,9 +129,15 @@ function SnapCardV1Inner({
107
129
  snap={snap}
108
130
  handlers={handlers}
109
131
  loading={loading}
132
+ loadingOverlay={null}
110
133
  />
111
134
  </View>
112
135
  </View>
136
+ {loading
137
+ ? loadingOverlay === undefined
138
+ ? <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
139
+ : loadingOverlay
140
+ : null}
113
141
  {isExpandable ? (
114
142
  <View
115
143
  style={[
@@ -170,6 +198,7 @@ export function SnapCardV1({
170
198
  borderRadius = 16,
171
199
  actionError,
172
200
  plain = false,
201
+ loadingOverlay,
173
202
  }: {
174
203
  snap: SnapPage;
175
204
  handlers: SnapActionHandlers;
@@ -179,6 +208,8 @@ export function SnapCardV1({
179
208
  borderRadius?: number;
180
209
  actionError?: string | null;
181
210
  plain?: boolean;
211
+ /** Custom content rendered while `loading` is true. Pass `null` to render nothing. */
212
+ loadingOverlay?: ReactNode;
182
213
  }) {
183
214
  return (
184
215
  <SnapThemeProvider appearance={appearance} colors={colors}>
@@ -190,6 +221,7 @@ export function SnapCardV1({
190
221
  actionError={actionError}
191
222
  appearance={appearance}
192
223
  plain={plain}
224
+ loadingOverlay={loadingOverlay}
193
225
  />
194
226
  </SnapThemeProvider>
195
227
  );