Skip to content

Commit

Permalink
feat: cast actions (#214)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
dalechyn and jxom authored Apr 10, 2024
1 parent 8a29c4d commit 3fc8b5c
Show file tree
Hide file tree
Showing 17 changed files with 889 additions and 5 deletions.
5 changes: 5 additions & 0 deletions .changeset/silly-peas-wink.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"frog": patch
---

Implemented "Cast Actions" support via `.castAction` handler.
52 changes: 52 additions & 0 deletions playground/src/castAction.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Frog } from 'frog'
import { Button } from 'frog'

export const app = new Frog()
.frame('/', (c) =>
c.res({
image: (
<div
tw="flex"
style={{
alignItems: 'center',
background: 'linear-gradient(to right, #432889, #17101F)',
backgroundSize: '100% 100%',
flexDirection: 'column',
flexWrap: 'nowrap',
height: '100%',
justifyContent: 'center',
textAlign: 'center',
width: '100%',
}}
>
<div
style={{
color: 'white',
fontSize: 60,
fontStyle: 'normal',
letterSpacing: '-0.025em',
lineHeight: 1.4,
marginTop: 30,
padding: '0 120px',
whiteSpace: 'pre-wrap',
}}
>
Add Cast Action
</div>
</div>
),
intents: [
<Button.AddCastAction action="/action" name="Log This!" icon="log">
Add
</Button.AddCastAction>,
],
}),
)
.castAction('/action', async (c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
return c.res({ message: 'Action Succeeded' })
})
2 changes: 2 additions & 0 deletions playground/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { devtools } from 'frog/dev'
import * as hubs from 'frog/hubs'
import { Box, Heading, vars } from './ui.js'

import { app as castActionApp } from './castAction.js'
import { app as clock } from './clock.js'
import { app as fontsApp } from './fonts.js'
import { app as middlewareApp } from './middleware.js'
Expand Down Expand Up @@ -178,6 +179,7 @@ export const app = new Frog({
],
})
})
.route('/castAction', castActionApp)
.route('/clock', clock)
.route('/ui', uiSystemApp)
.route('/fonts', fontsApp)
Expand Down
159 changes: 159 additions & 0 deletions site/pages/concepts/cast-actions.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Cast Actions

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)).

## Overview

At a glance:

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.
2. When the user presses the Cast Action button in the App, the App will make a `POST` request to the `.castAction` route.
3. Frame performs any action and returns a response to the App.

## Walkthrough

Here is a trivial example on how to expose an action with a frame. We will break it down below.

:::code-group

```tsx twoslash [src/index.tsx]
// @noErrors
/** @jsxImportSource hono/jsx */
// ---cut---
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 "Log this!" Action
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
Add
</Button.AddCastAction>,
]
})
})

app.castAction('/log-this', (c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
return c.res({ message:'Action Succeeded' })
})
```

:::

::::steps

### 1. Render Frame & Add Action Intent

In the example above, we are rendering Add Action intent:

1. `action` property is used to set the path to the cast action route.
2. `name` property is used to set the name of the action. It must be less than 30 characters
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).

```tsx twoslash [src/index.tsx]
// @noErrors
/** @jsxImportSource hono/jsx */
import { Button, Frog, parseEther } from 'frog'
import { abi } from './abi'

export const app = new Frog()
// ---cut---
app.frame('/', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Add "Log this!" Action
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
Add
</Button.AddCastAction>,
]
})
})

// ...
```


### 2. Handle `/log-this` Requests

Without a route handler to handle the Action request, the Cast Action will be meaningless.

Thus, let's define a `/log-this` route to handle the the Cast Action:

```tsx twoslash [src/index.tsx]
// @noErrors
/** @jsxImportSource hono/jsx */
import { Button, Frog, parseEther } from 'frog'
import { abi } from './abi'

export const app = new Frog()
// ---cut---
app.frame('/', (c) => {
return c.res({
image: (
<div style={{ color: 'white', display: 'flex', fontSize: 60 }}>
Add "Log this!" Action
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
Add
</Button.AddCastAction>,
]
})
})

app.castAction('/log-this', (c) => { // [!code focus]
console.log( // [!code focus]
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${ // [!code focus]
c.actionData.fid // [!code focus]
}`, // [!code focus]
) // [!code focus]
return c.res({ message: 'Action Succeeded' }) // [!code focus]
}) // [!code focus]
```

A breakdown of the `/log-this` route handler:

- `c.actionData` is never nullable and is always defined since Cast Actions always do `POST` request.
- We are responding with a `c.res` response and specifying a `message` that will appear in the success toast.


:::

### 5. Bonus: Learn the API

You can learn more about the transaction APIs here:

- [`Frog.castAction` Reference](/reference/frog-cast-action)
- [`Frog.castAction` Context Reference](/reference/frog-cast-action-context)
- [`Frog.castAction` Response Reference](/reference/frog-cast-action-response)

::::
123 changes: 123 additions & 0 deletions site/pages/reference/frog-cast-action-context.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Frog.castAction Context

The `c` object is the parameter of the route handlers. It contains context about the current cast action.

```tsx twoslash
// @noErrors
import { Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => { // [!code focus]
return c.res({/* ... */})
})
```

:::tip[Tip]
An action handler can also be asynchronous (ie. `async (c) => { ... }{:js}`).
:::

## actionData

- **Type**: `CastActionData`

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)

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => {
const { actionData } = c
const { castId, fid, messageHash, network, timestamp, url } = actionData // [!code focus]
return c.res({/* ... */})
})
```

## req

- **Type**: `Request`

[Hono request object](https://hono.dev/api/request).

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => {
const { req } = c // [!code focus]
return c.res({/* ... */})
})
```

## res

- **Type**: `(response: CastActionResponse) => CastActionResponse`

The action response.

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => {
return c.res({/* ... */}) // [!code focus]
})
```

## var

- **Type**: `HonoContext['var']`

Extract a context value that was previously set via [`set`](#set) in [Middleware](/concepts/middleware).

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.use(async (c, next) => {
c.set('message', 'Frog is cool!!')
await next()
})

app.castAction('/', (c) => {
const message = c.var.message // [!code focus]
return c.res({/* ... */})
})
```

## verified

- **Type**: `boolean`

Whether or not the [`actionData`](#actiondata) (and [`buttonIndex`](#buttonindex)) was verified by the Farcaster Hub API.

```tsx twoslash
// @noErrors
/** @jsxImportSource frog/jsx */
// ---cut---
import { Button, Frog } from 'frog'

export const app = new Frog()

app.castAction('/', (c) => {
const { verified } = c // [!code focus]
return c.res({/* ... */})
})
```
Loading

0 comments on commit 3fc8b5c

Please sign in to comment.