diff --git a/.changeset/old-buses-poke.md b/.changeset/old-buses-poke.md
new file mode 100644
index 00000000..46bb9393
--- /dev/null
+++ b/.changeset/old-buses-poke.md
@@ -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.
diff --git a/playground/src/castAction.tsx b/playground/src/castAction.tsx
index 2d3926a2..76cce0be 100644
--- a/playground/src/castAction.tsx
+++ b/playground/src/castAction.tsx
@@ -36,18 +36,24 @@ export const app = new Frog()
),
intents: [
-
- Add
- ,
+ Add,
],
}),
)
- .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!',
+ },
+ )
diff --git a/site/pages/concepts/cast-actions.mdx b/site/pages/concepts/cast-actions.mdx
index b5740049..d94a3acc 100644
--- a/site/pages/concepts/cast-actions.mdx
+++ b/site/pages/concepts/cast-actions.mdx
@@ -33,25 +33,25 @@ app.frame('/', (c) => {
),
intents: [
-
+
Add
,
]
})
})
-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" })
+)
```
:::
@@ -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
@@ -82,11 +80,7 @@ app.frame('/', (c) => {
),
intents: [
-
+
Add
,
]
@@ -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
@@ -119,25 +119,25 @@ app.frame('/', (c) => {
),
intents: [
-
+
Add
,
]
})
})
-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:
@@ -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:
diff --git a/site/pages/reference/frog-cast-action-context.mdx b/site/pages/reference/frog-cast-action-context.mdx
index 8bf8a5dc..a73a1038 100644
--- a/site/pages/reference/frog-cast-action-context.mdx
+++ b/site/pages/reference/frog-cast-action-context.mdx
@@ -10,7 +10,7 @@ export const app = new Frog()
app.castAction('/', (c) => { // [!code focus]
return c.res({/* ... */})
-})
+}, {/**/})
```
:::tip[Tip]
@@ -35,7 +35,7 @@ app.castAction('/', (c) => {
const { actionData } = c
const { castId, fid, messageHash, network, timestamp, url } = actionData // [!code focus]
return c.res({/* ... */})
-})
+}, {/**/})
```
## error
@@ -74,7 +74,7 @@ export const app = new Frog()
app.castAction('/', (c) => {
const { req } = c // [!code focus]
return c.res({/* ... */})
-})
+}, {/**/})
```
## res
@@ -93,7 +93,7 @@ export const app = new Frog()
app.castAction('/', (c) => {
return c.res({/* ... */}) // [!code focus]
-})
+}, {/**/})
```
## var
@@ -118,7 +118,7 @@ app.use(async (c, next) => {
app.castAction('/', (c) => {
const message = c.var.message // [!code focus]
return c.res({/* ... */})
-})
+}, {/**/})
```
## verified
@@ -138,5 +138,5 @@ export const app = new Frog()
app.castAction('/', (c) => {
const { verified } = c // [!code focus]
return c.res({/* ... */})
-})
+}, {/**/})
```
diff --git a/src/components/Button.tsx b/src/components/Button.tsx
index 49a2ff14..16cbe677 100644
--- a/src/components/Button.tsx
+++ b/src/components/Button.tsx
@@ -1,5 +1,4 @@
import type { HtmlEscapedString } from 'hono/utils/html'
-import type { Octicon } from '../types/octicon.js'
export const buttonPrefix = {
addCastAction: '_a',
@@ -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) {
@@ -67,7 +60,7 @@ export function ButtonAddCastAction({
,
,
] as unknown as HtmlEscapedString
}
diff --git a/src/frog-base.tsx b/src/frog-base.tsx
index 93164327..9693ee1f 100644
--- a/src/frog-base.tsx
+++ b/src/frog-base.tsx
@@ -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,
@@ -168,9 +169,22 @@ export type FrogConstructorParameters<
verify?: boolean | 'silent' | undefined
}
-export type RouteOptions = Pick & {
- fonts?: ImageOptions['fonts'] | (() => Promise)
-}
+export type RouteOptions = Pick<
+ FrogConstructorParameters,
+ 'verify'
+> &
+ (method extends 'frame'
+ ? {
+ fonts?: ImageOptions['fonts'] | (() => Promise)
+ }
+ : method extends 'castAction'
+ ? {
+ name: string
+ icon: Octicon
+ description?: string
+ aboutUrl?: string
+ }
+ : {})
/**
* A Frog instance.
@@ -296,18 +310,33 @@ export class FrogBase<
})
}
- castAction: HandlerInterface = (
+ castAction: HandlerInterface = (
...parameters: any[]
) => {
- const [path, middlewares, handler, options = {}] = getRouteParameters<
+ const [path, middlewares, handler, options] = getRouteParameters<
env,
- CastActionHandler
+ CastActionHandler,
+ '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({
context: await requestBodyToContext(c, {
hub:
@@ -342,7 +371,8 @@ export class FrogBase<
) => {
const [path, middlewares, handler, options = {}] = getRouteParameters<
env,
- FrameHandler
+ FrameHandler,
+ 'frame'
>(...parameters)
const { verify = this.verify } = options
@@ -726,7 +756,8 @@ export class FrogBase<
) => {
const [path, middlewares, handler, options = {}] = getRouteParameters<
env,
- TransactionHandler
+ TransactionHandler,
+ 'transaction'
>(...parameters)
const { verify = this.verify } = options
diff --git a/src/types/octicon.ts b/src/types/octicon.ts
index cc0b42e6..48a00157 100644
--- a/src/types/octicon.ts
+++ b/src/types/octicon.ts
@@ -1,126 +1,126 @@
export type Octicon =
- | 'number'
- | 'search'
- | 'image'
+ | 'accessibility'
| 'alert'
- | 'code'
- | 'meter'
- | 'ruby'
- | 'video'
- | 'filter'
- | 'stop'
- | 'plus'
- | 'info'
- | 'check'
- | 'book'
- | 'question'
- | 'mail'
- | 'home'
- | 'star'
- | 'inbox'
- | 'lock'
- | 'eye'
- | 'heart'
- | 'unlock'
- | 'play'
- | 'tag'
- | 'calendar'
- | 'database'
- | 'hourglass'
- | 'key'
- | 'gift'
- | 'sync'
| 'archive'
+ | 'beaker'
| 'bell'
- | 'bookmark'
- | 'briefcase'
- | 'bug'
- | 'clock'
- | 'credit-card'
- | 'globe'
- | 'infinity'
- | 'light-bulb'
- | 'location'
- | 'megaphone'
- | 'moon'
- | 'note'
- | 'pencil'
- | 'pin'
- | 'quote'
- | 'reply'
- | 'rocket'
- | 'shield'
- | 'stopwatch'
- | 'tools'
- | 'trash'
- | 'comment'
- | 'gear'
- | 'file'
- | 'hash'
- | 'square'
- | 'sun'
- | 'zap'
- | 'sign-out'
- | 'sign-in'
- | 'paste'
- | 'mortar-board'
- | 'history'
- | 'plug'
| 'bell-slash'
- | 'diamond'
- | 'id-badge'
- | 'person'
- | 'smiley'
- | 'pulse'
- | 'beaker'
- | 'flame'
- | 'people'
- | 'person-add'
- | 'broadcast'
- | 'graph'
- | 'shield-check'
- | 'shield-lock'
- | 'telescope'
- | 'webhook'
- | 'accessibility'
- | 'report'
- | 'verified'
| 'blocked'
+ | 'book'
+ | 'bookmark'
| 'bookmark-slash'
+ | 'briefcase'
+ | 'broadcast'
+ | 'bug'
+ | 'calendar'
+ | 'check'
| 'checklist'
| 'circle-slash'
+ | 'clock'
+ | 'code'
+ | 'comment'
+ | 'credit-card'
| 'cross-reference'
+ | 'database'
| 'dependabot'
| 'device-camera'
| 'device-camera-video'
| 'device-desktop'
| 'device-mobile'
+ | 'diamond'
| 'dot'
+ | 'eye'
| 'eye-closed'
+ | 'file'
+ | 'filter'
+ | 'flame'
+ | 'gear'
+ | 'gift'
+ | 'globe'
+ | 'graph'
+ | 'hash'
+ | 'heart'
+ | 'history'
+ | 'home'
+ | 'hourglass'
+ | 'id-badge'
+ | 'image'
+ | 'inbox'
+ | 'infinity'
+ | 'info'
| 'iterations'
+ | 'key'
| 'key-asterisk'
| 'law'
+ | 'light-bulb'
| 'link-external'
| 'list-ordered'
| 'list-unordered'
+ | 'location'
+ | 'lock'
| 'log'
+ | 'mail'
+ | 'megaphone'
| 'mention'
+ | 'meter'
| 'milestone'
+ | 'moon'
+ | 'mortar-board'
| 'mute'
| 'no-entry'
| 'north-star'
+ | 'note'
+ | 'number'
| 'organization'
| 'paintbrush'
| 'paper-airplane'
+ | 'paste'
+ | 'pencil'
+ | 'people'
+ | 'person'
+ | 'person-add'
+ | 'pin'
+ | 'play'
+ | 'plug'
+ | 'plus'
| 'project'
+ | 'pulse'
+ | 'question'
+ | 'quote'
+ | 'reply'
+ | 'report'
+ | 'rocket'
+ | 'ruby'
+ | 'search'
+ | 'shield'
+ | 'shield-check'
+ | 'shield-lock'
| 'shield-x'
+ | 'sign-in'
+ | 'sign-out'
| 'skip'
+ | 'smiley'
+ | 'square'
| 'squirrel'
| 'stack'
+ | 'star'
+ | 'stop'
+ | 'stopwatch'
+ | 'sun'
+ | 'sync'
+ | 'tag'
| 'tasklist'
+ | 'telescope'
| 'thumbsdown'
| 'thumbsup'
+ | 'tools'
+ | 'trash'
| 'typography'
+ | 'unlock'
| 'unmute'
- | 'workflow'
+ | 'verified'
| 'versions'
+ | 'video'
+ | 'webhook'
+ | 'workflow'
+ | 'zap'
diff --git a/src/types/response.ts b/src/types/response.ts
index f28620be..7d155844 100644
--- a/src/types/response.ts
+++ b/src/types/response.ts
@@ -6,7 +6,7 @@ export type BaseError = { message: string; statusCode?: ClientErrorStatusCode }
export type BaseErrorResponseFn = (response: BaseError) => TypedResponse
export type TypedResponse = {
- format: 'cast-action' | 'frame' | 'transaction'
+ format: 'castAction' | 'frame' | 'transaction'
} & OneOf<
{ data: data; status: 'success' } | { error: BaseError; status: 'error' }
>
diff --git a/src/types/routes.ts b/src/types/routes.ts
index be33e1b7..3334a830 100644
--- a/src/types/routes.ts
+++ b/src/types/routes.ts
@@ -83,7 +83,7 @@ export type H<
? FrameHandler
: M extends 'transaction'
? TransactionHandler
- : M extends 'cast-action'
+ : M extends 'castAction'
? CastActionHandler
: Handler
@@ -93,12 +93,12 @@ export type H<
////// //////
////////////////////////////////////////
-export interface HandlerInterface<
+export type HandlerInterface<
E extends Env = Env,
M extends string = string,
S extends Schema = {},
BasePath extends string = '/',
-> {
+> = {
// app.get(path, handler, options)
<
P extends string,
@@ -109,7 +109,9 @@ export interface HandlerInterface<
>(
path: P,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S & ToSchema, I['in'], MergeTypedResponseData>,
@@ -129,7 +131,9 @@ export interface HandlerInterface<
path: P,
middleware: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -153,7 +157,9 @@ export interface HandlerInterface<
middleware: MiddlewareHandler,
middleware_2: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -180,7 +186,9 @@ export interface HandlerInterface<
middleware_2: MiddlewareHandler,
middleware_3: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -210,7 +218,9 @@ export interface HandlerInterface<
middleware_3: MiddlewareHandler,
middleware_4: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -243,7 +253,9 @@ export interface HandlerInterface<
middleware_4: MiddlewareHandler,
middleware_5: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -279,7 +291,9 @@ export interface HandlerInterface<
middleware_5: MiddlewareHandler,
middleware_6: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -318,7 +332,9 @@ export interface HandlerInterface<
middleware_6: MiddlewareHandler,
middleware_7: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -360,7 +376,9 @@ export interface HandlerInterface<
middleware_7: MiddlewareHandler,
middleware_8: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
@@ -407,7 +425,9 @@ export interface HandlerInterface<
middleware_8: MiddlewareHandler,
middleware_9: MiddlewareHandler,
handler: H,
- options?: RouteOptions,
+ ...rest: M extends 'castAction'
+ ? [options: RouteOptions]
+ : [options?: RouteOptions]
): FrogBase<
E,
S &
diff --git a/src/utils/getCastActionContext.ts b/src/utils/getCastActionContext.ts
index 91332337..4a2f17e9 100644
--- a/src/utils/getCastActionContext.ts
+++ b/src/utils/getCastActionContext.ts
@@ -36,7 +36,7 @@ export function getCastActionContext<
env,
error: (data) => ({
error: data,
- format: 'cast-action',
+ format: 'castAction',
status: 'error',
}),
actionData: {
@@ -51,7 +51,7 @@ export function getCastActionContext<
req,
res: (data) => ({
data,
- format: 'cast-action',
+ format: 'castAction',
status: 'success',
}),
var: context.var,
diff --git a/src/utils/getRouteParameters.ts b/src/utils/getRouteParameters.ts
index b126fa10..4e8d1d64 100644
--- a/src/utils/getRouteParameters.ts
+++ b/src/utils/getRouteParameters.ts
@@ -2,10 +2,23 @@ import type { RouteOptions } from '../frog-base.js'
import type { Env } from '../types/env.js'
import type { MiddlewareHandler } from '../types/routes.js'
-export function getRouteParameters(
+export function getRouteParameters<
+ env extends Env,
+ handler,
+ method extends string,
+>(
...parameters: any[]
-): [string, MiddlewareHandler[], handler, RouteOptions] {
- const options: RouteOptions | undefined =
+): [
+ string,
+ MiddlewareHandler[],
+ handler,
+ method extends 'castAction'
+ ? RouteOptions
+ : RouteOptions | undefined,
+] {
+ const options: method extends 'castAction'
+ ? RouteOptions
+ : RouteOptions | undefined =
typeof parameters[parameters.length - 1] === 'object'
? parameters[parameters.length - 1]
: undefined
@@ -17,5 +30,5 @@ export function getRouteParameters(
else middlewares.push(parameters[i])
}
- return [parameters[0], middlewares, handler!, options ?? {}]
+ return [parameters[0], middlewares, handler!, options] as const
}