diff --git a/.changeset/chatty-ears-wash.md b/.changeset/chatty-ears-wash.md new file mode 100644 index 00000000..eb132714 --- /dev/null +++ b/.changeset/chatty-ears-wash.md @@ -0,0 +1,5 @@ +--- +"frog": patch +--- + +Widened handler return types to allow [`Response` objects](https://developer.mozilla.org/en-US/docs/Web/API/Response). diff --git a/playground/src/transaction.tsx b/playground/src/transaction.tsx index 97f515bd..f7518760 100644 --- a/playground/src/transaction.tsx +++ b/playground/src/transaction.tsx @@ -2,8 +2,8 @@ import { Button, Frog } from 'frog' export const app = new Frog() -app.frame('/', () => { - return { +app.frame('/', (c) => { + return c.res({ image: (
Example @@ -14,7 +14,7 @@ app.frame('/', () => { Send Transaction, Mint, ], - } + }) }) // Raw transaction diff --git a/site/pages/reference/frog-transaction-response.mdx b/site/pages/reference/frog-transaction-response.mdx index 25b0a412..85267fa9 100644 --- a/site/pages/reference/frog-transaction-response.mdx +++ b/site/pages/reference/frog-transaction-response.mdx @@ -43,6 +43,21 @@ app.transaction('/raw-send', (c) => { }) ``` +:::tip +Frog also supports [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) objects as a return value of the `.transaction` handler. Useful for returning error responses. + +```tsx twoslash +// @noErrors +import { Frog, parseEther } from 'frog' + +export const app = new Frog() + +app.transaction('/send-ether', (c) => { + return new Response('dang', { status: 400 }) // [!code focus] +}) +``` +::: + ## Send Transaction (`c.send`) ### chainId diff --git a/src/frog-base.tsx b/src/frog-base.tsx index 23c16238..a238b9ba 100644 --- a/src/frog-base.tsx +++ b/src/frog-base.tsx @@ -15,6 +15,7 @@ import { type FrameResponse, } from './types/frame.js' import type { Hub } from './types/hub.js' +import type { HandlerResponse } from './types/response.js' import { type Pretty } from './types/utils.js' import { fromQuery } from './utils/fromQuery.js' import { getButtonValues } from './utils/getButtonValues.js' @@ -261,7 +262,7 @@ export class FrogBase< path: path, handler: ( context: Pretty>, - ) => FrameResponse | Promise, + ) => HandlerResponse, options: RouteOptions = {}, ) { const { verify = this.verify } = options @@ -287,6 +288,9 @@ export class FrogBase< if (context.url !== parsePath(c.req.url)) return c.redirect(context.url) + const response = await handler(context) + if (response instanceof Response) return response + const { action, browserLocation = this.browserLocation, @@ -295,7 +299,7 @@ export class FrogBase< image, intents, title = 'Frog Frame', - } = await handler(context) + } = response.data const buttonValues = getButtonValues(parseIntents(intents)) if (context.status === 'redirect' && context.buttonIndex) { @@ -472,11 +476,14 @@ export class FrogBase< ? await this.imageOptions() : this.imageOptions + const response = await handler(context) + if (response instanceof Response) return response + const { image, headers = this.headers, imageOptions = defaultImageOptions, - } = await handler(context) + } = response.data if (typeof image === 'string') return c.redirect(image, 302) return new ImageResponse(image, { ...imageOptions, diff --git a/src/frog.tsx b/src/frog.tsx index 481a898f..d8a7dc28 100644 --- a/src/frog.tsx +++ b/src/frog.tsx @@ -4,6 +4,7 @@ import { routes as devRoutes } from './dev/routes.js' import { FrogBase, type RouteOptions } from './frog-base.js' import { type FrameContext } from './types/context.js' import { type FrameResponse } from './types/frame.js' +import type { HandlerResponse } from './types/response.js' import { type Pretty } from './types/utils.js' /** @@ -46,7 +47,7 @@ export class Frog< path: path, handler: ( context: Pretty>, - ) => FrameResponse | Promise, + ) => HandlerResponse, options: RouteOptions = {}, ) { super.frame(path, handler, options) diff --git a/src/routes/transaction.ts b/src/routes/transaction.ts index 80d3455b..dac76c40 100644 --- a/src/routes/transaction.ts +++ b/src/routes/transaction.ts @@ -2,6 +2,7 @@ import type { Env } from 'hono' import type { FrogBase, RouteOptions } from '../frog-base.js' import type { TransactionContext } from '../types/context.js' +import type { HandlerResponse } from '../types/response.js' import type { TransactionResponse } from '../types/transaction.js' import { getTransactionContext } from '../utils/getTransactionContext.js' import { parsePath } from '../utils/parsePath.js' @@ -12,7 +13,7 @@ export function transaction( path: string, handler: ( context: TransactionContext, - ) => TransactionResponse | Promise, + ) => HandlerResponse, options: RouteOptions = {}, ) { const { verify = this.verify } = options @@ -27,7 +28,8 @@ export function transaction( }), req: c.req, }) - const transaction = await handler(transactionContext) - return c.json(transaction) + const response = await handler(transactionContext) + if (response instanceof Response) return response + return c.json(response.data) }) } diff --git a/src/types/frame.ts b/src/types/frame.ts index da309444..3b470a73 100644 --- a/src/types/frame.ts +++ b/src/types/frame.ts @@ -1,4 +1,5 @@ import { type ImageResponseOptions } from 'hono-og' +import type { TypedResponse } from './response.js' export type FrameResponse = { /** @@ -105,7 +106,9 @@ export type FrameResponse = { title?: string | undefined } -export type FrameResponseFn = (response: FrameResponse) => FrameResponse +export type FrameResponseFn = ( + response: FrameResponse, +) => TypedResponse export type FrameData = { buttonIndex?: 1 | 2 | 3 | 4 | undefined diff --git a/src/types/response.ts b/src/types/response.ts new file mode 100644 index 00000000..693fc9af --- /dev/null +++ b/src/types/response.ts @@ -0,0 +1,10 @@ +export type TypedResponse = { + data: data + format: 'frame' | 'transaction' +} + +export type HandlerResponse = + | Response + | TypedResponse + | Promise + | Promise> diff --git a/src/types/transaction.ts b/src/types/transaction.ts index b4f8a233..7ad8d3e4 100644 --- a/src/types/transaction.ts +++ b/src/types/transaction.ts @@ -5,6 +5,7 @@ import type { GetValue, Hex, } from 'viem' +import type { TypedResponse } from './response.js' import type { UnionWiden, Widen } from './utils.js' ////////////////////////////////////////////////////// @@ -38,7 +39,7 @@ export type EthSendTransactionParameters = { export type TransactionResponseFn = ( parameters: parameters, -) => TransactionResponse +) => TypedResponse ////////////////////////////////////////////////////// // Send Transaction @@ -85,4 +86,4 @@ export type ContractTransactionResponseFn = < >, >( response: ContractTransactionParameters, -) => TransactionResponse +) => TypedResponse diff --git a/src/utils/getFrameContext.ts b/src/utils/getFrameContext.ts index 199580e5..feaba2d3 100644 --- a/src/utils/getFrameContext.ts +++ b/src/utils/getFrameContext.ts @@ -61,7 +61,7 @@ export function getFrameContext( previousButtonValues, previousState: previousState as any, req, - res: (data) => data, + res: (data) => ({ data, format: 'frame' }), status, transactionId: frameData?.transactionId, url, diff --git a/src/utils/getTransactionContext.ts b/src/utils/getTransactionContext.ts index c4e1daa5..d58f0000 100644 --- a/src/utils/getTransactionContext.ts +++ b/src/utils/getTransactionContext.ts @@ -81,7 +81,7 @@ export function getTransactionContext( }, } if (value) response.params.value = value.toString() - return response + return { data: response, format: 'transaction' } }, send(parameters) { return this.res({