Mutli-step Cast Actions
Multi-step Cast Actions are similar to Cast Actions with a difference that they return a frame, instead of showing a message. (see the spec).
Overview
At a glance:
- User installs Cast Action via specific deeplink or by clicking on
<Button.AddCastAction>
element with a specified target.castAction
route in a Frame. - When the user presses the Cast Action button in the App, the App will make a
POST
request to the.castAction
route. - Server performs any action and returns a response to the App, which is shown as an interactible Frame dialog.
Walkthrough
Here is a trivial example on how to expose a multi-step action with a frame. We will break it down below.
1. Render Frame & Add Action Intent
In the example above, we are rendering Add Action intent:
action
property is used to set the path to the cast action route.
app.frame('/', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Add "Hello world!" Action
</div>
),
intents: [
<Button.AddCastAction action="/hello-world">
Add
</Button.AddCastAction>,
]
})
})
// ...
2. Handle /hello-world
Requests
Without a route handler to handle the Action request, the Multi-step Cast Action will be meaningless.
To specify the name and icon for your action, the next properties are used in the action handler definition:
name
property is used to set the name of the action. It must be less than 30 charactersicon
property is used to associate your Multi-step Cast Action with one of the Octicons. You can see the supported list here.- (optional)
description
property is used to describe your action, up to 80 characters. - (optional)
aboutUrl
property is used to show an "About" link when installing an action.
Let's define a /hello-world
route to handle the the Multi-step Cast Action:
app.frame('/', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Add "Hello world!" Action
</div>
),
intents: [
<Button.AddCastAction
action="/hello-world"
>
Add
</Button.AddCastAction>,
]
})
})
app.castAction(
'/hello-world',
(c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
return c.res({ type: 'frame', action: '/hello-world-frame' })
},
{ name: "Hello world!", icon: "smiley" })
)
A breakdown of the /hello-world
route handler:
c.actionData
is never nullable and is always defined since Multi-step Cast Actions always doPOST
request.- We are responding with a
c.res
response and specifying amessage
that will appear in the success toast.
3. Defining a frame handler
Since in the previous step we send a response with "type": "frame"
and have specified "action": "/hello-world-frame"
, Client won't be able to resolve it since we have not defined it yet.
So let's define one!
import { Button, Frog, TextInput, parseEther } from 'frog'
import { abi } from './abi'
export const app = new Frog()
app.frame('/', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Add "Hello world!" Action
</div>
),
intents: [
<Button.AddCastAction action="/hello-world">
Add
</Button.AddCastAction>,
]
})
})
app.castAction(
'/hello-world',
(c) => {
return c.res({ type: 'frame', action: '/hello-world-frame' })
},
{ name: "Hello world!", icon: "smiley" })
)
app.frame('/hello-world-frame', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Hello world!
</div>
)
})
})
Now we're all set! You can use existing Frame API to connect multiple frames together to have a multi-step experience. (see the Connecting Frames (Actions)).
4. Bonus: Shorthand c.frame
In order not to add property "type": "frame"
to your c.res(...)
, you can use a shorthand c.frame(...)
.
app.castAction(
'/hello-world',
(c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
return c.frame({ action: '/hello-world-frame' })
},
{ name: "Hello world!", icon: "smiley" })
)
4. Bonus: Learn the API
You can learn more about the transaction APIs here: