@farcaster/snap 2.9.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +1 -1
  3. package/dist/react/catalog-renderer.d.ts +5 -5
  4. package/dist/react/catalog-renderer.js +16 -4
  5. package/dist/react/components/action-button.js +23 -5
  6. package/dist/react/index.d.ts +2 -1
  7. package/dist/react/snap-view-core.js +90 -25
  8. package/dist/react/v1/snap-view.js +1 -1
  9. package/dist/react/v2/snap-view.js +1 -1
  10. package/dist/react-native/components/snap-action-button.js +6 -1
  11. package/dist/react-native/snap-view-core.js +77 -24
  12. package/dist/react-native/types.d.ts +2 -1
  13. package/dist/render-state.d.ts +9 -0
  14. package/dist/render-state.js +27 -0
  15. package/dist/schemas.d.ts +123 -3
  16. package/dist/schemas.js +53 -2
  17. package/dist/server/parseRequest.js +19 -3
  18. package/dist/ui/button.d.ts +1 -0
  19. package/dist/ui/button.js +1 -0
  20. package/dist/ui/catalog.d.ts +13 -0
  21. package/dist/ui/catalog.js +15 -8
  22. package/package.json +1 -1
  23. package/src/index.ts +7 -0
  24. package/src/react/catalog-renderer.tsx +57 -3
  25. package/src/react/components/action-button.tsx +32 -3
  26. package/src/react/index.tsx +4 -1
  27. package/src/react/snap-view-core.tsx +144 -27
  28. package/src/react/v1/snap-view.tsx +1 -0
  29. package/src/react/v2/snap-view.tsx +1 -0
  30. package/src/react-native/components/snap-action-button.tsx +6 -1
  31. package/src/react-native/snap-view-core.tsx +114 -27
  32. package/src/react-native/types.ts +4 -1
  33. package/src/render-state.ts +46 -0
  34. package/src/schemas.ts +73 -2
  35. package/src/server/parseRequest.ts +37 -6
  36. package/src/ui/button.ts +1 -0
  37. package/src/ui/catalog.ts +16 -8
@@ -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
- applyStatePaths,
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
- onRenderStateChangeRef.current?.(cloneSnapRenderState(stateRef.current));
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),
@@ -256,11 +328,30 @@ export function SnapViewCoreInner({
256
328
  default:
257
329
  break;
258
330
  }
259
- }, []);
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);
260
346
 
261
347
  return (
262
- <View style={styles.container}>
263
- {loading ? (
348
+ <View
349
+ style={styles.container}
350
+ onStartShouldSetResponderCapture={() =>
351
+ hasPendingSnapAction(stateRef.current)
352
+ }
353
+ >
354
+ {showLoadingOverlay ? (
264
355
  loadingOverlay === undefined ? (
265
356
  <SnapLoadingOverlay appearance={mode} accentHex={accentHex} />
266
357
  ) : (
@@ -271,12 +362,8 @@ export function SnapViewCoreInner({
271
362
  <SnapCatalogView
272
363
  key={pageKey}
273
364
  spec={spec}
274
- state={initialState}
365
+ store={stateStore}
275
366
  loading={false}
276
- onStateChange={(changes) => {
277
- applyStatePaths(stateRef.current, changes);
278
- onRenderStateChange?.(cloneSnapRenderState(stateRef.current));
279
- }}
280
367
  onAction={handleAction}
281
368
  />
282
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
 
@@ -50,5 +51,7 @@ export type SnapActionHandlers = {
50
51
  recipientAddress?: string;
51
52
  }) => void;
52
53
  swap_token: (params: { sellToken?: string; buyToken?: string }) => void;
53
- send_transaction?: (params: SnapSendTransactionParams) => void;
54
+ send_transaction?: (
55
+ params: SnapSendTransactionParams,
56
+ ) => void | Promise<void | SnapTransactionResult>;
54
57
  };
@@ -7,6 +7,7 @@ export type SnapRenderStateChanges =
7
7
  | undefined;
8
8
 
9
9
  const SNAP_RENDER_STATE_META_KEY = "__snapRender";
10
+ const ACTION_ACTIVITY_KEY_MAX_LENGTH = 64;
10
11
 
11
12
  function isRecord(value: unknown): value is Record<string, unknown> {
12
13
  return typeof value === "object" && value !== null && !Array.isArray(value);
@@ -121,6 +122,51 @@ export function applyStatePaths(
121
122
  }
122
123
  }
123
124
 
125
+ function sanitizeActionActivityKey(value: string): string {
126
+ const sanitized = value
127
+ .trim()
128
+ .slice(0, ACTION_ACTIVITY_KEY_MAX_LENGTH)
129
+ .replace(/[^A-Za-z0-9_.-]/g, "_");
130
+ return sanitized || "action";
131
+ }
132
+
133
+ function getActionActivityKey(
134
+ actionName: unknown,
135
+ params: Record<string, unknown>,
136
+ ): string {
137
+ const explicitKey = params.activityKey;
138
+ return sanitizeActionActivityKey(
139
+ typeof explicitKey === "string" && explicitKey.trim()
140
+ ? explicitKey
141
+ : String(actionName || "action"),
142
+ );
143
+ }
144
+
145
+ export function buildActionActivityStateChanges({
146
+ actionName,
147
+ params,
148
+ pending,
149
+ }: {
150
+ actionName: unknown;
151
+ params: Record<string, unknown>;
152
+ pending: boolean;
153
+ }): { path: string; value: unknown }[] {
154
+ const key = getActionActivityKey(actionName, params);
155
+ return [
156
+ { path: `/actions/${key}/name`, value: String(actionName || "action") },
157
+ { path: `/actions/${key}/pending`, value: pending },
158
+ ];
159
+ }
160
+
161
+ export function hasPendingSnapAction(model: SnapRenderState): boolean {
162
+ const actions = model.actions;
163
+ if (!isRecord(actions)) return false;
164
+
165
+ return Object.values(actions).some(
166
+ (action) => isRecord(action) && action.pending === true,
167
+ );
168
+ }
169
+
124
170
  export function getUnpresentedSnapEffects(
125
171
  model: SnapRenderState,
126
172
  effects: readonly string[] | undefined,
package/src/schemas.ts CHANGED
@@ -107,10 +107,9 @@ const surfaceSchema = z.discriminatedUnion("type", [
107
107
  const fidSchema = z.number().int().nonnegative();
108
108
  const userSchema = z.object({ fid: fidSchema });
109
109
 
110
- export const payloadSchema = z
110
+ const basePayloadSchema = z
111
111
  .object({
112
112
  fid: fidSchema.optional(), // deprecated in favor of user.fid
113
- inputs: z.record(z.string(), postInputValueSchema).default({}),
114
113
  timestamp: z.number().int(),
115
114
  audience: z.string(),
116
115
  user: userSchema,
@@ -118,6 +117,58 @@ export const payloadSchema = z
118
117
  })
119
118
  .strip();
120
119
 
120
+ const snapSendTransactionParamsSchema = z
121
+ .object({
122
+ chainId: z.string(),
123
+ to: z.string(),
124
+ data: z.string().optional(),
125
+ value: z.string().optional(),
126
+ gas: z.string().optional(),
127
+ gasPrice: z.string().optional(),
128
+ maxFeePerGas: z.string().optional(),
129
+ maxPriorityFeePerGas: z.string().optional(),
130
+ })
131
+ .strict();
132
+
133
+ export type SnapSendTransactionParams = z.infer<
134
+ typeof snapSendTransactionParamsSchema
135
+ >;
136
+
137
+ const snapTransactionSuccessSchema = z
138
+ .object({
139
+ success: z.literal(true),
140
+ transactionHash: z.string(),
141
+ })
142
+ .strict();
143
+
144
+ const snapTransactionFailureSchema = z
145
+ .object({
146
+ success: z.literal(false),
147
+ reason: z
148
+ .enum(["rejected_by_user", "failed", "unknown"])
149
+ .optional()
150
+ .default("unknown"),
151
+ message: z.string().optional(),
152
+ code: z.union([z.string(), z.number()]).optional(),
153
+ transactionHash: z.string().optional(),
154
+ })
155
+ .strict();
156
+
157
+ export const snapTransactionResultSchema = z.discriminatedUnion("success", [
158
+ snapTransactionSuccessSchema,
159
+ snapTransactionFailureSchema,
160
+ ]);
161
+
162
+ export type SnapTransactionResult = z.infer<
163
+ typeof snapTransactionResultSchema
164
+ >;
165
+
166
+ export const payloadSchema = basePayloadSchema
167
+ .extend({
168
+ inputs: z.record(z.string(), postInputValueSchema).default({}),
169
+ })
170
+ .strip();
171
+
121
172
  export type SnapPayload = z.infer<typeof payloadSchema>;
122
173
 
123
174
  /** JFS payload shape for POST minus deprecated `fid`; used for GET auth via payload header. */
@@ -127,6 +178,7 @@ export type SnapGetPayload = z.infer<typeof getPayloadSchema>;
127
178
 
128
179
  export const ACTION_TYPE_GET = "get" as const;
129
180
  export const ACTION_TYPE_POST = "post" as const;
181
+ export const ACTION_TYPE_TRANSACTION_RESULT = "transaction_result" as const;
130
182
 
131
183
  const snapGetActionSchema = z.object({
132
184
  type: z.literal(ACTION_TYPE_GET),
@@ -144,9 +196,28 @@ const snapPostActionSchema = payloadSchema.extend({
144
196
 
145
197
  export type SnapPostAction = z.infer<typeof snapPostActionSchema>;
146
198
 
199
+ export const transactionResultPayloadSchema = basePayloadSchema
200
+ .extend({
201
+ type: z.literal(ACTION_TYPE_TRANSACTION_RESULT),
202
+ transaction: z
203
+ .object({
204
+ request: snapSendTransactionParamsSchema,
205
+ result: snapTransactionResultSchema,
206
+ })
207
+ .strict(),
208
+ })
209
+ .strip();
210
+
211
+ export type SnapTransactionResultPayload = z.infer<
212
+ typeof transactionResultPayloadSchema
213
+ >;
214
+
215
+ export type SnapTransactionResultAction = SnapTransactionResultPayload;
216
+
147
217
  export const snapActionSchema = z.discriminatedUnion("type", [
148
218
  snapGetActionSchema,
149
219
  snapPostActionSchema,
220
+ transactionResultPayloadSchema,
150
221
  ]);
151
222
 
152
223
  export type SnapAction = z.infer<typeof snapActionSchema>;
@@ -1,12 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import {
3
+ ACTION_TYPE_TRANSACTION_RESULT,
3
4
  ACTION_TYPE_GET,
4
5
  ACTION_TYPE_POST,
5
6
  getPayloadSchema,
6
7
  payloadSchema,
8
+ transactionResultPayloadSchema,
7
9
  type SnapAction,
8
10
  type SnapPayload,
9
11
  type SnapGetPayload,
12
+ type SnapTransactionResultPayload,
10
13
  } from "../schemas";
11
14
  import { decodePayload, parseJfs, verifyJFS } from "./verify";
12
15
  import { SNAP_PAYLOAD_HEADER } from "../constants";
@@ -121,9 +124,14 @@ async function parsePostRequest(
121
124
  request: Request,
122
125
  options: ParseRequestOptions,
123
126
  ): Promise<ParseRequestResult> {
124
- const result = await validateJfsPayload({
127
+ const result = await validateJfsPayload<
128
+ SnapPayload | SnapTransactionResultPayload
129
+ >({
125
130
  jfsText: await request.text(),
126
- schema: payloadSchema,
131
+ schema: (decodedPayload) =>
132
+ isTransactionResultPayload(decodedPayload)
133
+ ? transactionResultPayloadSchema
134
+ : payloadSchema,
127
135
  request,
128
136
  options,
129
137
  });
@@ -142,12 +150,30 @@ async function parsePostRequest(
142
150
  };
143
151
  }
144
152
 
153
+ if (isTransactionResultPayload(payload)) {
154
+ return {
155
+ success: true,
156
+ action: payload,
157
+ };
158
+ }
159
+
145
160
  return {
146
161
  success: true,
147
162
  action: { type: ACTION_TYPE_POST, ...payload },
148
163
  };
149
164
  }
150
165
 
166
+ function isTransactionResultPayload(
167
+ payload: unknown,
168
+ ): payload is { type: typeof ACTION_TYPE_TRANSACTION_RESULT } {
169
+ return (
170
+ payload !== null &&
171
+ typeof payload === "object" &&
172
+ "type" in payload &&
173
+ payload.type === ACTION_TYPE_TRANSACTION_RESULT
174
+ );
175
+ }
176
+
151
177
  /**
152
178
  * Shared pipeline for authenticated snap requests: parse the JFS envelope,
153
179
  * decode and schema-validate the payload, optionally verify the JFS signature
@@ -156,7 +182,9 @@ async function parsePostRequest(
156
182
  *
157
183
  * Both GET (payload header) and POST (request body) feed into this.
158
184
  */
159
- async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
185
+ async function validateJfsPayload<
186
+ T extends SnapPayload | SnapGetPayload | SnapTransactionResultPayload,
187
+ >({
160
188
  jfsText,
161
189
  schema,
162
190
  request,
@@ -164,7 +192,7 @@ async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
164
192
  invalidJsonMessage,
165
193
  }: {
166
194
  jfsText: string;
167
- schema: z.ZodType<T>;
195
+ schema: z.ZodType | ((decodedPayload: unknown) => z.ZodType);
168
196
  request: Request;
169
197
  options: ParseRequestOptions;
170
198
  invalidJsonMessage?: string;
@@ -183,14 +211,17 @@ async function validateJfsPayload<T extends SnapPayload | SnapGetPayload>({
183
211
  }
184
212
  const jfs = parsed.jfs;
185
213
 
186
- const payloadParsed = schema.safeParse(decodePayload(jfs.payload));
214
+ const decodedPayload = decodePayload(jfs.payload);
215
+ const selectedSchema =
216
+ typeof schema === "function" ? schema(decodedPayload) : schema;
217
+ const payloadParsed = selectedSchema.safeParse(decodedPayload);
187
218
  if (!payloadParsed.success) {
188
219
  return {
189
220
  ok: false,
190
221
  error: { type: "validation", issues: payloadParsed.error.issues },
191
222
  };
192
223
  }
193
- const payload = payloadParsed.data;
224
+ const payload = payloadParsed.data as T;
194
225
 
195
226
  if (!options.skipJFSVerification) {
196
227
  const verified = await verifyJFS(jfs);
package/src/ui/button.ts CHANGED
@@ -8,6 +8,7 @@ export const buttonProps = z.object({
8
8
  label: z.string().min(1).max(BUTTON_MAX_LABEL_CHARS),
9
9
  variant: z.enum(BUTTON_VARIANTS).optional(),
10
10
  icon: z.enum(ICON_NAMES).optional(),
11
+ disabled: z.boolean().optional(),
11
12
  });
12
13
 
13
14
  export type ButtonProps = z.infer<typeof buttonProps>;
package/src/ui/catalog.ts CHANGED
@@ -23,6 +23,10 @@ const snapClientParams = z.object({
23
23
  client_action: z.record(z.string(), z.unknown()),
24
24
  });
25
25
 
26
+ const activityParams = {
27
+ activityKey: z.string().min(1).max(64).optional(),
28
+ };
29
+
26
30
  /**
27
31
  * json-render catalog for snap elements.
28
32
  *
@@ -121,32 +125,32 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
121
125
  submit: {
122
126
  description:
123
127
  "POST to snap server with signed body (fid, inputs, timestamp, signature); response is next snap page.",
124
- params: z.object({ target: z.string() }),
128
+ params: z.object({ target: z.string(), ...activityParams }),
125
129
  },
126
130
  open_url: {
127
131
  description: "Open external URL in browser.",
128
- params: z.object({ target: z.string() }),
132
+ params: z.object({ target: z.string(), ...activityParams }),
129
133
  },
130
134
  open_snap: {
131
135
  description:
132
136
  "Open a snap URL inline. The client renders the target as a snap rather than opening a browser.",
133
- params: z.object({ target: z.string() }),
137
+ params: z.object({ target: z.string(), ...activityParams }),
134
138
  },
135
139
  open_mini_app: {
136
140
  description: "Open target URL as a Farcaster mini app.",
137
- params: z.object({ target: z.string() }),
141
+ params: z.object({ target: z.string(), ...activityParams }),
138
142
  },
139
143
  view_cast: {
140
144
  description: "Navigate to a cast by hash.",
141
- params: z.object({ hash: z.string() }),
145
+ params: z.object({ hash: z.string(), ...activityParams }),
142
146
  },
143
147
  view_profile: {
144
148
  description: "Navigate to a user profile by FID.",
145
- params: z.object({ fid: z.number() }),
149
+ params: z.object({ fid: z.number(), ...activityParams }),
146
150
  },
147
151
  view_channel: {
148
152
  description: "Navigate to a Farcaster channel by channel key.",
149
- params: z.object({ channelKey: z.string() }),
153
+ params: z.object({ channelKey: z.string(), ...activityParams }),
150
154
  },
151
155
  compose_cast: {
152
156
  description: "Open the cast composer with optional pre-filled content.",
@@ -154,11 +158,12 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
154
158
  text: z.string().optional(),
155
159
  channelKey: z.string().optional(),
156
160
  embeds: z.array(z.string()).optional(),
161
+ ...activityParams,
157
162
  }),
158
163
  },
159
164
  view_token: {
160
165
  description: "View a token in the wallet. Token is a CAIP-19 identifier.",
161
- params: z.object({ token: z.string() }),
166
+ params: z.object({ token: z.string(), ...activityParams }),
162
167
  },
163
168
  send_token: {
164
169
  description: "Open send flow for a token. Token is CAIP-19.",
@@ -167,6 +172,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
167
172
  amount: z.string().optional(),
168
173
  recipientFid: z.number().optional(),
169
174
  recipientAddress: z.string().optional(),
175
+ ...activityParams,
170
176
  }),
171
177
  },
172
178
  swap_token: {
@@ -174,6 +180,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
174
180
  params: z.object({
175
181
  sellToken: z.string().optional(),
176
182
  buyToken: z.string().optional(),
183
+ ...activityParams,
177
184
  }),
178
185
  },
179
186
  send_transaction: {
@@ -188,6 +195,7 @@ export const snapJsonRenderCatalog = defineCatalog(snapJsonRenderSchema, {
188
195
  gasPrice: z.string().optional(),
189
196
  maxFeePerGas: z.string().optional(),
190
197
  maxPriorityFeePerGas: z.string().optional(),
198
+ ...activityParams,
191
199
  }),
192
200
  },
193
201
  paginator_next: {