Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.

Commit 3fc8b5c

Browse files
dalechynjxom
andauthored
feat: cast actions (#214)
* feat: cast actions (wip) * nit: tweaks * feat: add `origin` to context to construct urls easier * fix: typecheck * nit: lint * nit: deeplink update https://warpcast.com/horsefacts.eth/0x6b2240df * fix: bad origins Picked up a global `origin` value lol. * fix: more origin fixes, replaced icons * nit: rename add action button * docs: add docs on cast actions * feat: add util `.message` and `.error` methods to action response * nit: line break Co-authored-by: jxom <jakemoxey@gmail.com> * nit: delete line break Co-authored-by: jxom <jakemoxey@gmail.com> * refactor: remove `.message` and `.error` helpers * feat: remove `origin` from `context`, use action for URL In devtools, `ButtonAddAction` doesn't currently works as '&' symbol is being unintentionally escaped with 'amp;'. * nit: lint * nit: tweaks Co-authored-by: jxom <jakemoxey@gmail.com> * refactor: `.action` to `.castAction` * nit: tweak * nit: rename button property * nit: tweak docs * chore: changeset --------- Co-authored-by: jxom <jakemoxey@gmail.com>
1 parent 8a29c4d commit 3fc8b5c

File tree

17 files changed

+889
-5
lines changed

17 files changed

+889
-5
lines changed

.changeset/silly-peas-wink.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"frog": patch
3+
---
4+
5+
Implemented "Cast Actions" support via `.castAction` handler.

playground/src/castAction.tsx

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Frog } from 'frog'
2+
import { Button } from 'frog'
3+
4+
export const app = new Frog()
5+
.frame('/', (c) =>
6+
c.res({
7+
image: (
8+
<div
9+
tw="flex"
10+
style={{
11+
alignItems: 'center',
12+
background: 'linear-gradient(to right, #432889, #17101F)',
13+
backgroundSize: '100% 100%',
14+
flexDirection: 'column',
15+
flexWrap: 'nowrap',
16+
height: '100%',
17+
justifyContent: 'center',
18+
textAlign: 'center',
19+
width: '100%',
20+
}}
21+
>
22+
<div
23+
style={{
24+
color: 'white',
25+
fontSize: 60,
26+
fontStyle: 'normal',
27+
letterSpacing: '-0.025em',
28+
lineHeight: 1.4,
29+
marginTop: 30,
30+
padding: '0 120px',
31+
whiteSpace: 'pre-wrap',
32+
}}
33+
>
34+
Add Cast Action
35+
</div>
36+
</div>
37+
),
38+
intents: [
39+
<Button.AddCastAction action="/action" name="Log This!" icon="log">
40+
Add
41+
</Button.AddCastAction>,
42+
],
43+
}),
44+
)
45+
.castAction('/action', async (c) => {
46+
console.log(
47+
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
48+
c.actionData.fid
49+
}`,
50+
)
51+
return c.res({ message: 'Action Succeeded' })
52+
})

playground/src/index.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { devtools } from 'frog/dev'
44
import * as hubs from 'frog/hubs'
55
import { Box, Heading, vars } from './ui.js'
66

7+
import { app as castActionApp } from './castAction.js'
78
import { app as clock } from './clock.js'
89
import { app as fontsApp } from './fonts.js'
910
import { app as middlewareApp } from './middleware.js'
@@ -178,6 +179,7 @@ export const app = new Frog({
178179
],
179180
})
180181
})
182+
.route('/castAction', castActionApp)
181183
.route('/clock', clock)
182184
.route('/ui', uiSystemApp)
183185
.route('/fonts', fontsApp)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# Cast Actions
2+
3+
Cast Actions let developers create custom buttons which users can install into their action bar on any Farcaster application (see the [spec](https://warpcast.notion.site/Spec-Farcaster-Actions-84d5a85d479a43139ea883f6823d8caa)).
4+
5+
## Overview
6+
7+
At a glance:
8+
9+
1. User installs Cast Action via specific deeplink or by clicking on `<Button.AddCastAction>{:jsx}` element with a specified target `.castAction` route in a Frame.
10+
2. When the user presses the Cast Action button in the App, the App will make a `POST` request to the `.castAction` route.
11+
3. Frame performs any action and returns a response to the App.
12+
13+
## Walkthrough
14+
15+
Here is a trivial example on how to expose an action with a frame. We will break it down below.
16+
17+
:::code-group
18+
19+
```tsx twoslash [src/index.tsx]
20+
// @noErrors
21+
/** @jsxImportSource hono/jsx */
22+
// ---cut---
23+
import { Button, Frog, TextInput, parseEther } from 'frog'
24+
import { abi } from './abi'
25+
26+
export const app = new Frog()
27+
28+
app.frame('/', (c) => {
29+
return c.res({
30+
image: (
31+
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
32+
Add "Log this!" Action
33+
</div>
34+
),
35+
intents: [
36+
<Button.AddCastAction
37+
action="/log-this"
38+
name="Log This!"
39+
icon="log"
40+
>
41+
Add
42+
</Button.AddCastAction>,
43+
]
44+
})
45+
})
46+
47+
app.castAction('/log-this', (c) => {
48+
console.log(
49+
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
50+
c.actionData.fid
51+
}`,
52+
)
53+
return c.res({ message:'Action Succeeded' })
54+
})
55+
```
56+
57+
:::
58+
59+
::::steps
60+
61+
### 1. Render Frame & Add Action Intent
62+
63+
In the example above, we are rendering Add Action intent:
64+
65+
1. `action` property is used to set the path to the cast action route.
66+
2. `name` property is used to set the name of the action. It must be less than 30 characters
67+
3. `icon` property is used to associate your Cast Action with one of the Octicons. You can see the supported list [here](https://warpcast.notion.site/Spec-Farcaster-Actions-84d5a85d479a43139ea883f6823d8caa).
68+
69+
```tsx twoslash [src/index.tsx]
70+
// @noErrors
71+
/** @jsxImportSource hono/jsx */
72+
import { Button, Frog, parseEther } from 'frog'
73+
import { abi } from './abi'
74+
75+
export const app = new Frog()
76+
// ---cut---
77+
app.frame('/', (c) => {
78+
return c.res({
79+
image: (
80+
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
81+
Add "Log this!" Action
82+
</div>
83+
),
84+
intents: [
85+
<Button.AddCastAction
86+
action="/log-this"
87+
name="Log This!"
88+
icon="log"
89+
>
90+
Add
91+
</Button.AddCastAction>,
92+
]
93+
})
94+
})
95+
96+
// ...
97+
```
98+
99+
100+
### 2. Handle `/log-this` Requests
101+
102+
Without a route handler to handle the Action request, the Cast Action will be meaningless.
103+
104+
Thus, let's define a `/log-this` route to handle the the Cast Action:
105+
106+
```tsx twoslash [src/index.tsx]
107+
// @noErrors
108+
/** @jsxImportSource hono/jsx */
109+
import { Button, Frog, parseEther } from 'frog'
110+
import { abi } from './abi'
111+
112+
export const app = new Frog()
113+
// ---cut---
114+
app.frame('/', (c) => {
115+
return c.res({
116+
image: (
117+
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
118+
Add "Log this!" Action
119+
</div>
120+
),
121+
intents: [
122+
<Button.AddCastAction
123+
action="/log-this"
124+
name="Log This!"
125+
icon="log"
126+
>
127+
Add
128+
</Button.AddCastAction>,
129+
]
130+
})
131+
})
132+
133+
app.castAction('/log-this', (c) => { // [!code focus]
134+
console.log( // [!code focus]
135+
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${ // [!code focus]
136+
c.actionData.fid // [!code focus]
137+
}`, // [!code focus]
138+
) // [!code focus]
139+
return c.res({ message: 'Action Succeeded' }) // [!code focus]
140+
}) // [!code focus]
141+
```
142+
143+
A breakdown of the `/log-this` route handler:
144+
145+
- `c.actionData` is never nullable and is always defined since Cast Actions always do `POST` request.
146+
- We are responding with a `c.res` response and specifying a `message` that will appear in the success toast.
147+
148+
149+
:::
150+
151+
### 5. Bonus: Learn the API
152+
153+
You can learn more about the transaction APIs here:
154+
155+
- [`Frog.castAction` Reference](/reference/frog-cast-action)
156+
- [`Frog.castAction` Context Reference](/reference/frog-cast-action-context)
157+
- [`Frog.castAction` Response Reference](/reference/frog-cast-action-response)
158+
159+
::::
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
# Frog.castAction Context
2+
3+
The `c` object is the parameter of the route handlers. It contains context about the current cast action.
4+
5+
```tsx twoslash
6+
// @noErrors
7+
import { Frog } from 'frog'
8+
9+
export const app = new Frog()
10+
11+
app.castAction('/', (c) => { // [!code focus]
12+
return c.res({/* ... */})
13+
})
14+
```
15+
16+
:::tip[Tip]
17+
An action handler can also be asynchronous (ie. `async (c) => { ... }{:js}`).
18+
:::
19+
20+
## actionData
21+
22+
- **Type**: `CastActionData`
23+
24+
Data from the action that was passed via the `POST` body from a Farcaster Client. [See more.](https://warpcast.notion.site/Spec-Farcaster-Actions-84d5a85d479a43139ea883f6823d8caa)
25+
26+
```tsx twoslash
27+
// @noErrors
28+
/** @jsxImportSource frog/jsx */
29+
// ---cut---
30+
import { Button, Frog } from 'frog'
31+
32+
export const app = new Frog()
33+
34+
app.castAction('/', (c) => {
35+
const { actionData } = c
36+
const { castId, fid, messageHash, network, timestamp, url } = actionData // [!code focus]
37+
return c.res({/* ... */})
38+
})
39+
```
40+
41+
## req
42+
43+
- **Type**: `Request`
44+
45+
[Hono request object](https://hono.dev/api/request).
46+
47+
```tsx twoslash
48+
// @noErrors
49+
/** @jsxImportSource frog/jsx */
50+
// ---cut---
51+
import { Button, Frog } from 'frog'
52+
53+
export const app = new Frog()
54+
55+
app.castAction('/', (c) => {
56+
const { req } = c // [!code focus]
57+
return c.res({/* ... */})
58+
})
59+
```
60+
61+
## res
62+
63+
- **Type**: `(response: CastActionResponse) => CastActionResponse`
64+
65+
The action response.
66+
67+
```tsx twoslash
68+
// @noErrors
69+
/** @jsxImportSource frog/jsx */
70+
// ---cut---
71+
import { Button, Frog } from 'frog'
72+
73+
export const app = new Frog()
74+
75+
app.castAction('/', (c) => {
76+
return c.res({/* ... */}) // [!code focus]
77+
})
78+
```
79+
80+
## var
81+
82+
- **Type**: `HonoContext['var']`
83+
84+
Extract a context value that was previously set via [`set`](#set) in [Middleware](/concepts/middleware).
85+
86+
```tsx twoslash
87+
// @noErrors
88+
/** @jsxImportSource frog/jsx */
89+
// ---cut---
90+
import { Button, Frog } from 'frog'
91+
92+
export const app = new Frog()
93+
94+
app.use(async (c, next) => {
95+
c.set('message', 'Frog is cool!!')
96+
await next()
97+
})
98+
99+
app.castAction('/', (c) => {
100+
const message = c.var.message // [!code focus]
101+
return c.res({/* ... */})
102+
})
103+
```
104+
105+
## verified
106+
107+
- **Type**: `boolean`
108+
109+
Whether or not the [`actionData`](#actiondata) (and [`buttonIndex`](#buttonindex)) was verified by the Farcaster Hub API.
110+
111+
```tsx twoslash
112+
// @noErrors
113+
/** @jsxImportSource frog/jsx */
114+
// ---cut---
115+
import { Button, Frog } from 'frog'
116+
117+
export const app = new Frog()
118+
119+
app.castAction('/', (c) => {
120+
const { verified } = c // [!code focus]
121+
return c.res({/* ... */})
122+
})
123+
```

0 commit comments

Comments
 (0)