diff --git a/examples/_dev/src/index.tsx b/examples/_dev/src/index.tsx index 244bec15..3bf6d734 100644 --- a/examples/_dev/src/index.tsx +++ b/examples/_dev/src/index.tsx @@ -3,7 +3,7 @@ import { Button, Farc, TextInput } from 'farc' import { app as todoApp } from './todos' const app = new Farc({ - // basePath: '/api' + // basePath: '/api', }) app.frame('/', (context) => { diff --git a/src/farc-base.tsx b/src/farc-base.tsx index a67d4d63..71af0558 100644 --- a/src/farc-base.tsx +++ b/src/farc-base.tsx @@ -12,6 +12,7 @@ import { } from './types.js' import { deserializeJson } from './utils/deserializeJson.js' import { getFrameContext } from './utils/getFrameContext.js' +import { getIntentData } from './utils/getIntentData.js' import { parseIntents } from './utils/parseIntents.js' import { parsePath } from './utils/parsePath.js' import { requestToContext } from './utils/requestToContext.js' @@ -44,6 +45,7 @@ export class FarcBase< > { #initialState: state = undefined as state + basePath: string hono: Hono fetch: Hono['fetch'] get: Hono['get'] @@ -56,6 +58,7 @@ export class FarcBase< }: FarcConstructorParameters = {}) { this.hono = new Hono(honoOptions) if (basePath) this.hono = this.hono.basePath(basePath) + this.basePath = basePath ?? '/' this.fetch = this.hono.fetch.bind(this.hono) this.get = this.hono.get.bind(this.hono) this.post = this.hono.post.bind(this.hono) @@ -67,20 +70,15 @@ export class FarcBase< path: path, handler: ( context: FrameContext, - previousContext: PreviousFrameContext | undefined, ) => FrameHandlerReturnType | Promise, ) { // Frame Route (implements GET & POST). this.hono.use(parsePath(path), async (c) => { const query = c.req.query() - const url = new URL(c.req.url) - const baseUrl = `${url.origin}${url.pathname}` const previousContext = query.previousContext - ? deserializeJson>( - query.previousContext, - ) + ? deserializeJson>(query.previousContext) : undefined const context = await getFrameContext({ context: await requestToContext(c.req), @@ -94,22 +92,19 @@ export class FarcBase< if (!location) throw new Error('location required to redirect') return c.redirect(location, 302) } + if (context.url !== parsePath(c.req.url)) return c.redirect(context.url) - if (context.url !== parsePath(c.req.url)) - return c.redirect( - `${context.url}?previousContext=${query.previousContext}`, - ) - - const { action, imageAspectRatio, intents } = await handler( - context, - previousContext, - ) + const { action, imageAspectRatio, intents } = await handler(context) const parsedIntents = intents ? parseIntents(intents) : null + const intentData = getIntentData(parsedIntents) - const serializedContext = serializeJson(context) - const serializedPreviousContext = serializeJson({ + const serializedContext = serializeJson({ ...context, - intents: parsedIntents, + request: undefined, + }) + const serializedPreviousContext = serializeJson({ + initialUrl: context.initialUrl, + intentData, previousState: context.deriveState(), }) @@ -141,7 +136,11 @@ export class FarcBase< {parsedIntents} @@ -175,9 +174,7 @@ export class FarcBase< this.hono.get(`${parsePath(path)}/image`, async (c) => { const query = c.req.query() const previousContext = query.previousContext - ? deserializeJson>( - query.previousContext, - ) + ? deserializeJson>(query.previousContext) : undefined const context = await getFrameContext({ context: deserializeJson>(query.context), @@ -185,7 +182,7 @@ export class FarcBase< previousContext, request: c.req, }) - const { image } = await handler(context, previousContext) + const { image } = await handler(context) return new ImageResponse(image) }) } diff --git a/src/farc.tsx b/src/farc.tsx index 61d7778c..88787886 100644 --- a/src/farc.tsx +++ b/src/farc.tsx @@ -1,7 +1,7 @@ import type { Env, Schema } from 'hono' import { routes as devRoutes } from './dev/routes.js' import { FarcBase, type FrameHandlerReturnType } from './farc-base.js' -import type { FrameContext, PreviousFrameContext } from './types.js' +import type { FrameContext } from './types.js' export class Farc< state = undefined, @@ -13,7 +13,6 @@ export class Farc< path: path, handler: ( context: FrameContext, - previousContext: PreviousFrameContext | undefined, ) => FrameHandlerReturnType | Promise, ) { super.frame(path, handler) diff --git a/src/types.ts b/src/types.ts index f681f59a..7e099434 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,12 +1,11 @@ // TODO: TSDoc import { type Context, type Env } from 'hono' -import { type JSXNode } from 'hono/jsx' export type FrameContext = { - buttonIndex?: number | undefined buttonValue?: string | undefined deriveState: (fn?: (state: state) => void) => state + frameData: FrameData initialUrl: string inputText?: string | undefined previousState: state @@ -18,17 +17,14 @@ export type FrameContext = { * - `response` - The frame has been interacted with (user presses button). */ status: 'initial' | 'redirect' | 'response' - trustedData?: TrustedData | undefined - untrustedData?: UntrustedData | undefined url: Context['req']['url'] } -export type PreviousFrameContext< - path extends string = string, - state = unknown, -> = FrameContext & { - /** Intents from the previous frame. */ - intents: readonly JSXNode[] +export type PreviousFrameContext = { + initialUrl: string + /** Intent data from the previous frame. */ + intentData: readonly Record[] + previousState: state } export type FrameData = { @@ -49,6 +45,8 @@ export type FrameVersion = 'vNext' export type FrameIntent = JSX.Element | false | null | undefined export type FrameIntents = FrameIntent | FrameIntent[] +export type FrameIntentData = Record + export type TrustedData = { messageBytes: string } diff --git a/src/utils/getFrameContext.ts b/src/utils/getFrameContext.ts index 2a64b5ba..e21c2d0f 100644 --- a/src/utils/getFrameContext.ts +++ b/src/utils/getFrameContext.ts @@ -5,12 +5,9 @@ import { getIntentState } from './getIntentState.js' import { parsePath } from './parsePath.js' type GetFrameContextParameters = { - context: Pick< - FrameContext, - 'status' | 'trustedData' | 'untrustedData' | 'url' - > + context: Pick, 'frameData' | 'status' | 'url'> initialState?: state - previousContext: PreviousFrameContext | undefined + previousContext: PreviousFrameContext | undefined request: Context['req'] } @@ -18,14 +15,12 @@ export async function getFrameContext( options: GetFrameContextParameters, ): Promise> { const { context, previousContext, request } = options - const { trustedData, untrustedData } = context || {} + const { frameData } = context || {} - const { buttonIndex, buttonValue, inputText, redirect, reset } = - getIntentState( - // TODO: derive from untrusted data. - untrustedData, - previousContext?.intents || [], - ) + const { buttonValue, inputText, redirect, reset } = getIntentState( + frameData, + previousContext?.intentData || [], + ) const status = (() => { if (redirect) return 'redirect' @@ -50,16 +45,14 @@ export async function getFrameContext( } return { - buttonIndex, buttonValue, + frameData, initialUrl, inputText, deriveState, previousState: previousState as any, request, status, - trustedData, - untrustedData, url, } } diff --git a/src/utils/getIntentData.ts b/src/utils/getIntentData.ts new file mode 100644 index 00000000..3ee2359d --- /dev/null +++ b/src/utils/getIntentData.ts @@ -0,0 +1,25 @@ +import type { JSXNode } from 'hono/jsx' +import type { FrameIntentData } from '../types.js' + +export function getIntentData( + intents: readonly JSXNode[] | null, +): FrameIntentData[] { + if (!intents) return [] + + const intentData: FrameIntentData[] = [] + for (const intent of intents) { + if (!intent) continue + const { property } = intent.props + const data: FrameIntentData = {} + for (const [key, value] of Object.entries(intent.props)) { + if (!key.startsWith('data-')) continue + data[key] = value + } + if (Object.keys(data).length === 0) continue + intentData.push({ + property, + ...data, + }) + } + return intentData +} diff --git a/src/utils/getIntentState.ts b/src/utils/getIntentState.ts index ce7236a2..01b7db2e 100644 --- a/src/utils/getIntentState.ts +++ b/src/utils/getIntentState.ts @@ -1,33 +1,36 @@ -import { type JSXNode } from 'hono/jsx' +import { type FrameData, type FrameIntentData } from '../types.js' -import { type FrameData } from '../types.js' +type IntentState = { + buttonValue: string | undefined + inputText: string | undefined + redirect: boolean + reset: boolean +} export function getIntentState( frameData: FrameData | undefined, - intents: readonly JSXNode[] | null, + intentData: readonly FrameIntentData[] | null, ) { const { buttonIndex, inputText } = frameData || {} - - const state = { - buttonIndex, + const state: IntentState = { buttonValue: undefined, inputText, redirect: false, reset: false, } - if (!intents) return state + if (!intentData) return state if (buttonIndex) { - const buttonIntents = intents.filter((intent) => - intent?.props?.property?.match(/fc:frame:button:\d$/), + const buttonIntents = intentData.filter((intent) => + intent?.property?.match(/fc:frame:button:\d$/), ) const intent = buttonIntents[buttonIndex - 1] - const type = intent.props['data-type'] + const type = intent['data-type'] if (type === 'redirect') state.redirect = true else if (type === 'reset') state.reset = true - const value = intent.props['data-value'] + const value = intent['data-value'] state.buttonValue = value } diff --git a/src/utils/requestToContext.ts b/src/utils/requestToContext.ts index 89234e35..a130b7de 100644 --- a/src/utils/requestToContext.ts +++ b/src/utils/requestToContext.ts @@ -3,19 +3,18 @@ import type { FrameContext } from '../types.js' type RequestToContextReturnType = Pick< FrameContext, - 'status' | 'trustedData' | 'untrustedData' | 'url' + 'frameData' | 'status' | 'url' > export async function requestToContext( request: Context['req'], ): Promise { - const { trustedData, untrustedData } = + const { trustedData: _trustedData, untrustedData } = (await request.json().catch(() => {})) || {} return { + frameData: untrustedData, status: request.method === 'POST' ? 'response' : 'initial', - trustedData, - untrustedData, url: request.url, } }