Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: cast actions deeplink v2 #251

Merged
merged 9 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/old-buses-poke.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
"frog": minor
---

Deprecated the Cast Actions Deeplink V1 format in favor of V2. [See more](https://warpcast.notion.site/Spec-Farcaster-Actions-84d5a85d479a43139ea883f6823d8caa).

Breaking changes have affected `Button.AddCastAction` and `.castAction` handler:
- `Button.AddCastAction` now only accepts `action` property;
- `.castAction` handler now requries a third parameter (`options`) to be set. Properties that were removed from `Button.AddCastAction` have migrated here, and `aboutUrl` and `description` were added along.
30 changes: 18 additions & 12 deletions playground/src/castAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,18 +36,24 @@ export const app = new Frog()
</div>
),
intents: [
<Button.AddCastAction action="/action" name="Log This!" icon="log">
Add
</Button.AddCastAction>,
<Button.AddCastAction action="/action">Add</Button.AddCastAction>,
],
}),
)
.castAction('/action', async (c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
if (Math.random() > 0.5) return c.error({ message: 'Action failed :(' })
return c.res({ message: 'Action Succeeded' })
})
.castAction(
'/action',
async (c) => {
console.log(
`Cast Action to ${JSON.stringify(c.actionData.castId)} from ${
c.actionData.fid
}`,
)
if (Math.random() > 0.5) return c.error({ message: 'Action failed :(' })
return c.res({ message: 'Action Succeeded' })
},
{
name: 'Log This!',
icon: 'log',
description: 'This cast action will log something!',
},
)
74 changes: 36 additions & 38 deletions site/pages/concepts/cast-actions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,25 +33,25 @@ app.frame('/', (c) => {
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
<Button.AddCastAction action="/log-this">
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' })
})
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' })
},
{ name: "Log This!", icon: "log" })
)
```

:::
Expand All @@ -62,9 +62,7 @@ app.castAction('/log-this', (c) => {

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).
`action` property is used to set the path to the cast action route.

```tsx twoslash [src/index.tsx]
// @noErrors
Expand All @@ -82,11 +80,7 @@ app.frame('/', (c) => {
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
<Button.AddCastAction action="/log-this">
Add
</Button.AddCastAction>,
]
Expand All @@ -101,7 +95,13 @@ app.frame('/', (c) => {

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:
To specify the name and icon for your action, the next properties are used in the action handler definition:
1. `name` property is used to set the name of the action. It must be less than 30 characters
2. `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).
3. (optional) `description` property is used to describe your action, up to 80 characters.
4. (optional) `aboutUrl` property is used to show an "About" link when installing an action.

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

```tsx twoslash [src/index.tsx]
// @noErrors
Expand All @@ -119,25 +119,25 @@ app.frame('/', (c) => {
</div>
),
intents: [
<Button.AddCastAction
action="/log-this"
name="Log This!"
icon="log"
>
<Button.AddCastAction action="/log-this">
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]
app.castAction(
'/log-this', // [!code focus]
(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]
{ name: "Log This!", icon: "log" }) // [!code focus]
) // [!code focus]
```

A breakdown of the `/log-this` route handler:
Expand All @@ -146,9 +146,7 @@ A breakdown of the `/log-this` route handler:
- We are responding with a `c.res` response and specifying a `message` that will appear in the success toast.


:::

### 5. Bonus: Learn the API
### 3. Bonus: Learn the API

You can learn more about the transaction APIs here:

Expand Down
12 changes: 6 additions & 6 deletions site/pages/reference/frog-cast-action-context.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export const app = new Frog()

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

:::tip[Tip]
Expand All @@ -35,7 +35,7 @@ app.castAction('/', (c) => {
const { actionData } = c
const { castId, fid, messageHash, network, timestamp, url } = actionData // [!code focus]
return c.res({/* ... */})
})
}, {/**/})
```

## error
Expand Down Expand Up @@ -74,7 +74,7 @@ export const app = new Frog()
app.castAction('/', (c) => {
const { req } = c // [!code focus]
return c.res({/* ... */})
})
}, {/**/})
```

## res
Expand All @@ -93,7 +93,7 @@ export const app = new Frog()

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

## var
Expand All @@ -118,7 +118,7 @@ app.use(async (c, next) => {
app.castAction('/', (c) => {
const message = c.var.message // [!code focus]
return c.res({/* ... */})
})
}, {/**/})
```

## verified
Expand All @@ -138,5 +138,5 @@ export const app = new Frog()
app.castAction('/', (c) => {
const { verified } = c // [!code focus]
return c.res({/* ... */})
})
}, {/**/})
```
9 changes: 1 addition & 8 deletions src/components/Button.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { HtmlEscapedString } from 'hono/utils/html'
import type { Octicon } from '../types/octicon.js'

export const buttonPrefix = {
addCastAction: '_a',
Expand Down Expand Up @@ -43,18 +42,12 @@ export function ButtonRoot({
export type ButtonAddCastActionProps = ButtonProps & {
/** Action path */
action: string
/** Name of the action. 30 characters maximum */
name: string
/** Octicon name. @see https://primer.style/foundations/icons */
icon: Octicon
}

ButtonAddCastAction.__type = 'button'
export function ButtonAddCastAction({
action,
children,
name,
icon,
// @ts-ignore - private
index = 1,
}: ButtonAddCastActionProps) {
Expand All @@ -67,7 +60,7 @@ export function ButtonAddCastAction({
<meta property={`fc:frame:button:${index}:action`} content="link" />,
<meta
property={`fc:frame:button:${index}:target`}
content={`https://warpcast.com/~/add-cast-action?postUrl=${action}&name=${name}&actionType=post&icon=${icon}`}
content={`https://warpcast.com/~/add-cast-action?url=${action}`}
/>,
] as unknown as HtmlEscapedString
}
Expand Down
53 changes: 42 additions & 11 deletions src/frog-base.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type {
ImageOptions,
} from './types/frame.js'
import type { Hub } from './types/hub.js'
import type { Octicon } from './types/octicon.js'
import type {
CastActionHandler,
FrameHandler,
Expand Down Expand Up @@ -168,9 +169,22 @@ export type FrogConstructorParameters<
verify?: boolean | 'silent' | undefined
}

export type RouteOptions = Pick<FrogConstructorParameters, 'verify'> & {
fonts?: ImageOptions['fonts'] | (() => Promise<ImageOptions['fonts']>)
}
export type RouteOptions<method extends string = string> = Pick<
FrogConstructorParameters,
'verify'
> &
(method extends 'frame'
? {
fonts?: ImageOptions['fonts'] | (() => Promise<ImageOptions['fonts']>)
}
: method extends 'castAction'
? {
name: string
icon: Octicon
description?: string
aboutUrl?: string
}
: {})

/**
* A Frog instance.
Expand Down Expand Up @@ -296,18 +310,33 @@ export class FrogBase<
})
}

castAction: HandlerInterface<env, 'cast-action', schema, basePath> = (
castAction: HandlerInterface<env, 'castAction', schema, basePath> = (
...parameters: any[]
) => {
const [path, middlewares, handler, options = {}] = getRouteParameters<
const [path, middlewares, handler, options] = getRouteParameters<
env,
CastActionHandler<env>
CastActionHandler<env>,
'castAction'
>(...parameters)

const { verify = this.verify } = options
const { verify = this.verify, ...installParameters } = options

// Cast Action Route (implements GET and POST).
this.hono.use(parseHonoPath(path), ...middlewares, async (c) => {
const url = getRequestUrl(c.req)
const origin = this.origin ?? url.origin
const baseUrl = origin + parsePath(this.basePath)

if (c.req.method === 'GET') {
return c.json({
...installParameters,
postUrl: baseUrl + parsePath(path),
action: {
type: 'post',
},
})
}

// Cast Action Route (implements POST).
this.hono.post(parseHonoPath(path), ...middlewares, async (c) => {
const { context } = getCastActionContext<env, string>({
context: await requestBodyToContext(c, {
hub:
Expand Down Expand Up @@ -342,7 +371,8 @@ export class FrogBase<
) => {
const [path, middlewares, handler, options = {}] = getRouteParameters<
env,
FrameHandler<env>
FrameHandler<env>,
'frame'
>(...parameters)

const { verify = this.verify } = options
Expand Down Expand Up @@ -726,7 +756,8 @@ export class FrogBase<
) => {
const [path, middlewares, handler, options = {}] = getRouteParameters<
env,
TransactionHandler<env>
TransactionHandler<env>,
'transaction'
>(...parameters)

const { verify = this.verify } = options
Expand Down