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 }