diff --git a/.changeset/new-emus-love.md b/.changeset/new-emus-love.md new file mode 100644 index 000000000..8386bcdd2 --- /dev/null +++ b/.changeset/new-emus-love.md @@ -0,0 +1,12 @@ +--- +"docs": patch +--- + +- adds default context reference to createFrames reference +- frames.js for apps section +- clean up example filenames, add steps +- createFrames params +- troubleshooting docs +- update useFrame reference +- make button docs more visible +- multi-page guide diff --git a/docs/pages/guides/display-frames.mdx b/docs/pages/guides/apps/display-frames.mdx similarity index 78% rename from docs/pages/guides/display-frames.mdx rename to docs/pages/guides/apps/display-frames.mdx index 6506f5617..298bd9fdd 100644 --- a/docs/pages/guides/display-frames.mdx +++ b/docs/pages/guides/apps/display-frames.mdx @@ -9,26 +9,32 @@ This guide shows you how to add frames rendering to your next.js + tailwind app ## Steps -1. Create a new repo +::::steps -`npx create-next-app@latest my-project --ts --eslint --tailwind --app` +### Create a new repo -`cd my-project` -`yarn add @frames.js/render` +Create a new Next.js app -`yarn install` +```sh +npx create-next-app@latest my-project --ts --eslint --tailwind --app +cd my-project +``` + +Add `@frames.js/render` to your project -2. Add proxies for routing frame requests via your backend for privacy + preventing CORS issues -```tsx filename="// ./app/frames/route.tsx" -// ./app/frames/route.tsx +```sh +yarn add @frames.js/render +``` + +### Add proxies for routing frame requests via your backend for privacy + preventing CORS issues +```tsx [./app/frames/route.tsx] export { GET, POST } from "@frames.js/render/next"; ``` -3. Add the renderer to your page +### Add the renderer to your page -```tsx filename="// ./app/page.tsx" -// ./app/page.tsx +```tsx [./app/page.tsx] "use client"; import { FrameUI, @@ -81,10 +87,9 @@ export default function Page() { ``` -4. In order for the styles to work, your project should have tailwind set up as well as the tailwind.config.js rule +### In order for the styles to work, your project should have tailwind set up as well as the tailwind.config.js rule -```tsx filename="// tailwind.config.js" -// tailwind.config.js +```tsx [tailwind.config.js] const config = { // ... content: [ @@ -97,10 +102,9 @@ const config = { } ``` -5. Allow images from any domain +### Allow images from any domain -```tsx filename="// next.config.js" -// next.config.js +```tsx [next.config.js] const nextConfig = { images: { remotePatterns: [ @@ -113,10 +117,10 @@ const nextConfig = { }; ``` -6. Run `yarn run dev` - -7. Done! πŸŽ‰ +### Run `yarn run dev` +### Done! πŸŽ‰ +:::: ### Optional diff --git a/docs/pages/guides/create-frame.mdx b/docs/pages/guides/create-frame.mdx index 0528d582d..3e29ef334 100644 --- a/docs/pages/guides/create-frame.mdx +++ b/docs/pages/guides/create-frame.mdx @@ -1,5 +1,5 @@ --- -title: "Guide: Display Frames in your app" +title: "Guide: Create your first Frame" description: "Frames.js is the react based framework for making frames. Debugger included." --- @@ -9,24 +9,38 @@ This guide shows you how to add frames rendering to your next.js + tailwind app ## Steps -1. Create a new repo +::::steps -`npx create-next-app@latest my-project --ts --eslint --tailwind --app` +### Create a new repo -`cd my-project` +Create a new Next.js app -`yarn add frames.js` +```sh +npx create-next-app@latest my-project --ts --eslint --tailwind --app +cd my-project +``` + +Add `frames.js` to your project + +```sh +yarn add frames.js +``` + +### Create your Frames app -`yarn install` +```tsx [./app/frames/frames.ts] +import { createFrames } from "frames.js/next"; -2. Create your Frames app +export const frames = createFrames(); +``` + +### Create a route -```tsx filename="// ./app/frames/route.tsx" -// ./app/frames/route.tsx +```tsx [./app/frames/route.tsx] /* eslint-disable react/jsx-key */ -import { createFrames, Button } from "frames.js/next"; +import { Button } from "frames.js/next"; +import { frames } from "./frames"; -const frames = createFrames(); const handleRequest = frames(async (ctx) => { return { image: ( @@ -37,10 +51,10 @@ const handleRequest = frames(async (ctx) => { ), buttons: [ - , - , ], @@ -51,10 +65,9 @@ export const GET = handleRequest; export const POST = handleRequest; ``` -3. If you have an existing page, render Frames in your metadata +### If you have an existing page, render Frames in your metadata -```tsx filename="// ./app/page.tsx" -// ./app/page.tsx +```tsx [./app/page.tsx] import { fetchMetadata } from "frames.js/next"; export async function generateMetadata() { @@ -62,7 +75,12 @@ export async function generateMetadata() { title: "My Page", // provide a full URL to your /frames endpoint other: await fetchMetadata( - new URL("/frames", process.env.VERCEL_URL ? `https://${process.env.VERCEL_URL}` : "http://localhost:3000") + new URL( + "/frames", + process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000" + ) ), }; } @@ -72,6 +90,12 @@ export default function Page() { } ``` -4. Run `yarn run dev` +### Run `yarn run dev` + +### Done! πŸŽ‰ + +:::: + +## Next Steps -5. Done! πŸŽ‰ +- Read the [`createFrames`](/reference/core/createFrames) and [`Button`](/reference/core/Button) documentation diff --git a/docs/pages/guides/middleware.mdx b/docs/pages/guides/middleware.mdx index d3e0de689..aa0d761e2 100644 --- a/docs/pages/guides/middleware.mdx +++ b/docs/pages/guides/middleware.mdx @@ -7,47 +7,143 @@ description: "" Frames.js uses middleware to extend the functionality of Frames, bringing in data from API providers, verifying frame actions and adding Open Frames support. -You can use middleware for all your frames by passing in middleware via the `middleware` Option. +You can use middleware for all your frames by passing in middleware via the `middleware` option in `createFrames` or you can specify per-route middleware. -```tsx -import { farcasterHubContext, openframes } from "frames.js/middleware"; -import { createFrames } from "frames.js/next"; -import { getXmtpFrameMessage, isXmtpFrameActionPayload } from "frames.js/xmtp"; +## Using middleware -const DEFAULT_DEBUGGER_URL = - process.env.DEBUGGER_URL ?? "http://localhost:3010/"; +Include the middleware in your `createFrames` call: -export const DEFAULT_DEBUGGER_HUB_URL = - process.env.NODE_ENV === "development" - ? new URL("/hub", DEFAULT_DEBUGGER_URL).toString() - : undefined; +```tsx [frames.ts] +import { farcasterHubContext } from "frames.js/middleware"; +import { createFrames } from "frames.js/next"; const frames = createFrames({ basePath: "/", initialState: { pageIndex: 0, }, - middleware: [ - farcasterHubContext({ - hubHttpUrl: DEFAULT_DEBUGGER_HUB_URL, - }), - openframes({ - clientProtocol: { - id: "xmtp", - version: "2024-02-09", - }, - handler: { - isValidPayload: (body: JSON) => isXmtpFrameActionPayload(body), - getFrameMessage: async (body: JSON) => { - if (!isXmtpFrameActionPayload(body)) { - return undefined; - } - const result = await getXmtpFrameMessage(body); - - return { ...result }; - }, - }, - }), - ], + middleware: [farcasterHubContext()], +}); +``` + +```tsx [frames/username/route.tsx] +import { frames } from "./frames"; + +export const POST = frames(async (ctx) => { + // The added context from the middleware will be available on `ctx` here + if (!ctx.message.isValid) { + throw new Error("Invalid message"); + } + + return { + image: ( +
+ The user's username is {ctx.message.requesterUserData.username} +
+ ), + }; +}); +``` + +### Per-route middleware + +You can also specify middleware per-route that will only be applied to that route: + +```tsx [frames/username/route.tsx] +import { farcasterHubContext } from "frames.js/middleware"; + +export const POST = frames( + async (ctx) => { + // The added context from the middleware will be available on `ctx` here + if (!ctx.message.isValid) { + throw new Error("Invalid message"); + } + + return { + image: ( +
+ The user's username is {ctx.message.requesterUserData.username} +
+ ), + }; + }, + { + middleware: [farcasterHubContext()], + } +); +``` + +## Defining your own middleware + +You can define your own middleware by creating a function that returns a promise that resolves to the next middleware, or a [Web API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), or a [`FrameDefinition`](/reference/core/createFrames#framedefinition). + +Middleware can modify the context or return a response that will terminate the request early. + +### Adding context + +:::code-group + +```tsx [frames.ts] +import { createFrames, types } from "frames.js/next"; + +const myMiddleware: types.FramesMiddleware = async ( + ctx, + next +) => { + return next({ foo: "bar" }); +}; + +export const frames = createFrames({ + basePath: "/", + initialState: { + pageIndex: 0, + }, + // Add the middleware + middleware: [myMiddleware], +}); +``` + +```tsx [frames/route.tsx] +import { Button } from "frames.js/next"; +import { frames } from "./frames"; + +const handler = frames(async (ctx) => { + return { + // Use the additional contect + image:
foo: ${ctx.bar}
, + }; +}); + +export const GET = handler; +export const POST = handler; +``` + +::: + + +### Accessing the request object + +Sometimes you want to access the request object in your middleware - whenever you do this, you should clone the request object to avoid mutating it and breaking other middleware. + +Here's an example of creating middleware which will add the request json to your context: + +```tsx +import { createFrames, types } from "frames.js/next"; + +const bodyMiddleware: types.FramesMiddleware = async ( + ctx, + next +) => { + const body = await ctx.request.clone().json(); + return next({ body }); +}; + +export const frames = createFrames({ + basePath: "/", + initialState: { + pageIndex: 0, + }, + middleware: [bodyMiddleware], + // The request body will now be available via `ctx.body` in your frame handlers }); -``` \ No newline at end of file +``` diff --git a/docs/pages/guides/multiple-frames.mdx b/docs/pages/guides/multiple-frames.mdx index 2cff40e25..37e30b68f 100644 --- a/docs/pages/guides/multiple-frames.mdx +++ b/docs/pages/guides/multiple-frames.mdx @@ -5,67 +5,168 @@ description: "" # Multi-Page Frames -You will want to connect multiple frames together. -There's two different ways of navigating between frames. +Frames.js can be used to create multi-page applications by defining multiple Frames that are linked together. -The first way is by defining the `state` prop of a `Button`, and using that state to return a different Frame in the handler. See below +## Creating a Multi-Page Application -```tsx -/* eslint-disable react/jsx-key */ -import { createFrames, Button } from "frames.js/next"; +Frames are connected by [`Button`](/reference/core/Button) targets, similar to how Next.js `Link` components work. + +:::steps + +### Create your frames app + +We create a new directory `./frames` with a `frames.ts` file to export our frames application from because it needs to be used from multiple routes. -const totalPages = 5; +```tsx [frames.ts] +import { createFrames } from "frames.js/next"; -const frames = createFrames({ - basePath: "/examples/new-api/frames", - initialState: { - pageIndex: 0, - }, +export const frames = createFrames({ + basePath: "/frames", }); +``` + +### Define your initial route -const handleRequest = frames(async (ctx) => { - const pageIndex = Number(ctx.searchParams.pageIndex || 0); +The first frame is always fetched via a GET request and is typically included alongside existing OpenGraph data via the [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) function in Next.js if you have an existing site. - const imageUrl = `https://picsum.photos/seed/frames.js-${pageIndex}/300/200`; +#### Define the initial frame +Create a `./frames/route.tsx` file that contains your initial frame. This frame will include buttons to navigate to other frames. + +```tsx [route.tsx] +/* eslint-disable react/jsx-key */ +import { frames } from "./frames"; +import { Button } from "frames.js/next"; + +export const GET = frames(async () => { return { - image: ( -
- Image -
- This is slide {pageIndex + 1} / {totalPages} -
-
- ), + image:
Welcome
, buttons: [ + // With query params , - , ], - textInput: "Type something!", }; }); +``` + +#### Export the initial frame metadata + +In your `page.tsx` file, fetch the initial frame's metadata and include it alongside your existing page's metadata. -export const GET = handleRequest; -export const POST = handleRequest; +`fetchMetadata` is a helper function that fetches the metadata for a frame from the frames.js handler and formats it for use in the `generateMetadata` function. + +```tsx [page.tsx] +import { fetchMetadata } from "frames.js/next"; + +export async function generateMetadata() { + return { + title: "My Page", + // provide a full URL to your /frames endpoint + other: await fetchMetadata( + new URL( + "/frames", + process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000" + ) + ), + }; +} + +export default function Page() { + return My existing page; +} ``` -The second way to navigate between frames is by defining a `Button` with `type`, `post`, with a `target` that points at another Frame. -This can be a Frame on the same domain, or a Frame on another website entirely. In order to link between Frames in the same project, you need to set up a frames.js handler on the `POST` route of the path defined in the target. +### Create the other routes + +Create additional frames in the `./frames` directory. + +#### Route 1 + +Create a directory `./frames/route1/route.tsx` with a `POST` handler that returns the frame content. + +```tsx [route1.tsx] +/* eslint-disable react/jsx-key */ +import { frames } from "../frames"; +import { Button } from "frames.js/next"; + +export const POST = frames(async (ctx) => { + const foo = ctx.searchParams.foo; + + return { + image:
Route 1 foo: {foo}
, // foo: bar + buttons: [ + , + ], + }; +}); +``` + +#### Route 2 + +Create a directory `./frames/route2/route.tsx` with a `POST` handler that returns the frame content. + +```tsx [route2.tsx] +/* eslint-disable react/jsx-key */ +import { frames } from "../frames"; +import { Button } from "frames.js/next"; + +export const POST = frames(async () => { + return { + image:
Route 2
, + buttons: [ + , + ], + }; +}); +``` + +### (Optional) Navigate back to the initial frame -{/* -TODO: Link to examples - */} \ No newline at end of file +If you want to navigate back to the initial frame you need to export a `POST` handler for the initial route. You may want to refactor the initial frame handler into a `frameHandler` variable that is exported as both `GET` and `POST` + +```tsx [route.tsx] +import { frames } from "./frames"; + +const frameHandler = frames(async () => { + return { + image:
Welcome
+ buttons: [ + , + , + ], + }; +}); + +export const GET = frameHandler; +export const POST = frameHandler; +``` + +You can then navigate back to the initial frame by linking to the initial route. + +```tsx + +``` + +::: + +## Notes + +The second way to navigate between frames is by defining a [`Button`](/reference/core/Button) with `type`, `post`, with a `target` that points at another Frame. +This can be a Frame on the same domain, or a Frame on another website entirely. In order to link between Frames in the same project, you need to set up a frames.js handler on the `POST` route of the path defined in the target. diff --git a/docs/pages/index.mdx b/docs/pages/index.mdx index 9238310c6..f3b845abf 100644 --- a/docs/pages/index.mdx +++ b/docs/pages/index.mdx @@ -59,9 +59,7 @@ pnpm create frames yarn add frames.js ``` -```tsx filename="// ./app/page.tsx" -// ./app/page.tsx - +```tsx [./app/page.tsx] import { fetchMetadata } from "frames.js/next"; export async function generateMetadata() { @@ -83,8 +81,7 @@ export default function Home(props) { } ``` -```ts filename="./app/frames/route.tsx" -// ./app/frames/route.tsx +```ts [./app/frames/route.tsx] /* eslint-disable react/jsx-key */ import { createFrames, Button } from 'frames.js/next'; @@ -120,14 +117,12 @@ Check out the following places for more Frames-related content: Or use the [hosted Frames debugger](https://debugger.framesjs.org/?url=https%3A%2F%2Fframesjs.org). Running locally has the benefits of it working with natively with localhost. -## Prefer to not use JSX? - -### Use frames.js in Next.js using helper functions +## Prefer to not use JSX? -```tsx filename="./app/page.tsx" -// page that renders a frame -// ./app/page.tsx +### frames.js in Next.js using helper functions +```tsx [./app/page.tsx] +// Page that returns a frame import { Frame, getFrameFlattened } from "frames.js"; import type { Metadata } from "next"; @@ -158,10 +153,8 @@ export const metadata: Metadata = { }; ``` -```ts filename="app/frames/route.ts" -// handle frame actions -// ./app/frames/route.ts - +```ts [./app/frames/route.ts] +// Route that handles frame actions import { getFrameHtml, validateFrameMessage } from "frames.js"; import { NextRequest } from "next/server"; diff --git a/docs/pages/reference/core/Button.mdx b/docs/pages/reference/core/Button.mdx index 38daa078a..e95a4d30b 100644 --- a/docs/pages/reference/core/Button.mdx +++ b/docs/pages/reference/core/Button.mdx @@ -20,15 +20,22 @@ This button sends a request to a URL on which it was rendered. If you want to na The `target` path will be resolved relatively to current URL. -###Β Passing state when button is clicked using query parameters +### Passing state between frames ```tsx - ``` -The state will be available in handler `ctx.pressedButton.state`. +The state will be available in the frame handler via `ctx.searchParams` e.g. + +```ts +ctx.searchParams.foo; // bar +``` ## Post Redirect Button @@ -39,7 +46,10 @@ It accepts same props as `post` button. ```tsx import { Button } from "frames.js/core"; -; ``` @@ -114,8 +124,7 @@ type EthSendTransactionParams { If the transaction is successful, the frame will send a POST request to the URL specified in `post_url` or the frame `post_url`. This can be handled by the frame to show a success message or redirect the user to a different page. -```tsx -// /route.tsx +```tsx [route.tsx] import { Button } from "frames.js/next"; import { createFrames } from "frames.js/next"; @@ -124,7 +133,7 @@ const export frames = createFrames() export const GET = frames(async (ctx) => { return { image: ( -
+
Execute transaction
), diff --git a/docs/pages/reference/core/createFrames.mdx b/docs/pages/reference/core/createFrames.mdx index f7b4f66ad..be7865df2 100644 --- a/docs/pages/reference/core/createFrames.mdx +++ b/docs/pages/reference/core/createFrames.mdx @@ -17,47 +17,124 @@ const handleRequest = frames(async (ctx) => { The function passed to `frames` will be called with the context of a frame action and should return a `FrameDefinition`. +## Parameters + +`createFrames` accepts an optional options object with the following properties: + +### `basePath` + +- Type: `string` + +A string that specifies the base path for all relative URLs in the frame definition. It defaults to `/`. + +### `initialState` + +- Type: generic + +A JSON serializable value that is used if no state is provided in the message or you are on the initial frame. + +### `middleware` + +See the [Middleware guide](/guides/middleware) for more information. + +Type: `FramesMiddleware` + +An array of middleware functions that are called before the frame handler and allows you to inject additional context into the `ctx` parameter passed to each frame handler call. + +Each middleware should return a promise that resolves to the next middleware, or a [Web API `Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response), or a `FrameDefinition`. + +#### Types + +For strong type support in the handler, the middleware should be typed as `FramesMiddleware`. + +```tsx +import { createFrames, types } from "frames.js/next"; + +const myMiddleware: types.FramesMiddleware< + any, + { foo?: string } +> = async (ctx, next) => { + return next({ foo: "bar" }); +}; +``` + +#### Example + +```tsx [./app/frames/route.tsx] +const frames = createFrames({ + middleware: [ + async (ctx, next) => { + console.log("Before frame handler"); + const result = await next({ name: "Alice" }); + console.log("After frame handler"); + return result; + }, + ], +}); + +const handler = frames(async (ctx) => { + return { + image: {ctx.name}, // Outputs an image with the text "Alice" + }; +}); +``` + ## FrameDefinition `FrameDefinition` is an object that describes a frame. It has the following properties: +### `image` + +- Type: `React.ReactElement | string` + +The image to be rendered in the frame. If a string is provided, it must be a valid URL. + +### `imageOptions` + +- Type: `{ aspectRatio?: "1.91:1" | "1:1" } & ConstructorParameters[1]` + +Options for the image. The `aspectRatio` property can be set to `"1.91:1"` or `"1:1"`. + +### `buttons` + +- Type: 1, 2, 3, or 4 `FrameButtonElement` elements + +An array of buttons to be rendered in the frame. The buttons are rendered in the order they are provided. + +#### Example + ```tsx -/** - * Frame definition, this is rendered by the frames - */ -export type FrameDefinition = { - /** - * If string then it must be a valid URL - */ - image: React.ReactElement | string; - imageOptions?: { - /** - * @default '1.91:1' - */ - aspectRatio?: "1.91:1" | "1:1"; - } & ConstructorParameters[1]; - buttons?: - | [] - | [FrameButtonElement] - | [FrameButtonElement, FrameButtonElement] - | [FrameButtonElement, FrameButtonElement, FrameButtonElement] - | [ - FrameButtonElement, - FrameButtonElement, - FrameButtonElement, - FrameButtonElement, - ]; - /** - * Label for text input, if no value is provided the input is not rendered - */ - textInput?: string; - /** - * Global app state that will be available on next frame - */ - state?: JsonValue; -} & ResponseInit; +import { Button } from "frames.js/next"; + +const handleRequest = frames(async (ctx) => { + return { + image: Test, + buttons: [ + , + , + ], + }; +}); ``` +### `textInput` + +- Type: `string` + +Label for text input. If no value is provided, the input is not rendered. + +### `state` + +- Type: `JsonValue` + +Global app state that will be available on the next frame. + +### `headers` + +- Type: `HeadersInit` + +Custom headers to be included in the response. + The `ResponseInit` properties allow you to specify custom headers such as `Cache-Control`. ### Cache-Control @@ -84,8 +161,7 @@ const handleRequest = frames(async (ctx) => { aspectRatio: "1:1", }, buttons: [], - headers: { - // [!code focus] + headers: {// [!code focus] // Max cache age in seconds // [!code focus] "Cache-Control": "max-age=0", // [!code focus] }, // [!code focus] @@ -102,3 +178,86 @@ handleRequest(new Request("/")).then((res) => { return res.text(); }); ``` + +### Per-route middleware + +You can also pass middleware to the `handleRequest` function to be executed only for that specific route. + +```tsx +const handleRequest = frames( + async (ctx) => { + return { + image: Test, + }; + }, + { + middleware: [farcasterHubContext({ hubHttpUrl: process.env.HUB_HTTP_URL })], + } +); +``` + +This will only execute the `farcasterHubContext` middleware for the route that `handleRequest` is called with. + +## Context + +Core middleware is included and executed by default and gives you access to the following default context in your frame handlers: + +### `basePath` + +- Type: `string` + +Specifies the base path for all relative URLs in the frame definition. + +### `initialState` + +- Type: generic + +A JSON serializable value that is used if no state is provided in the message or you are on the initial frame. + +### `request` + +- Type: [Web API `Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) + +The request object that was passed to the request handler. + +### `url` + +- Type: [Web API `URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL) + +The URL object that was parsed from the request. + +### `searchParams` + +- Type: `Record` + +The search params in the URL as an object. If there are no search params, it will be an empty object. + +### `pressedButton` + +- Type: `undefined | { action: "post" | "post_redirect"; index: 1 | 2 | 3 | 4 }` + +The button that was clicked on the previous frame. + +### `buttonIndex` + +- Type: `number` + +The index of the button that was clicked on the previous frame. + +### `message` + +- Type: `FrameMessage | undefined` + +The frame message that was parsed from the request body. + +### `clientProtocol` + +- Type: `ClientProtocolId | undefined` + +The client protocol that was used to send the frame message. + +### `state` + +- Type: `JsonValue` + +The state extracted from the frame message. If you are on the initial frame (no button pressed), the value is the `initialState` value passed to `createFrames`. If you are on a frame with a button pressed, the value is the state from the previous frame. diff --git a/docs/pages/reference/core/next/index.mdx b/docs/pages/reference/core/next/index.mdx index 12a719491..1324c6a91 100644 --- a/docs/pages/reference/core/next/index.mdx +++ b/docs/pages/reference/core/next/index.mdx @@ -8,8 +8,7 @@ Frames.js can be easily integrated with [Next.js](https://nextjs.org) applicatio Frames handler is responsible for rendering your Frames and also reacts to user interactions with buttons. -```tsx -// app/frames/route.tsx +```tsx [./app/frames/route.tsx] /* eslint-disable react/jsx-key */ import { createFrames, Button } from "frames.js/next"; @@ -29,8 +28,7 @@ export const POST = handleRequest; In order to render the initial frame of your Frames app on some of your pages, you need to render frame metadata. That can be achieved using [`generateMetadata() API`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata). -```tsx -// app/page.tsx +```tsx [./app/page.tsx] import { fetchMetadata } from "frames.js/next"; export async function generateMetadata() { diff --git a/docs/pages/reference/core/remix/index.mdx b/docs/pages/reference/core/remix/index.mdx index 5649e9e0a..2282f209a 100644 --- a/docs/pages/reference/core/remix/index.mdx +++ b/docs/pages/reference/core/remix/index.mdx @@ -8,8 +8,7 @@ Frames.js can be easily integrated with [Remix](https://remix.run) applications. Frames handler is responsible for rendering your Frames and also reacts to user interactions with buttons. -```tsx -// app/routes/frames.tsx +```tsx [./app/routes/frames.tsx] /* eslint-disable react/jsx-key */ import { createFrames, Button } from "frames.js/remix"; @@ -27,8 +26,7 @@ export const loader = handleRequest; ### Render initial frame on your existing page -```tsx -// app/routes/_index.tsx +```tsx [./app/routes/_index.tsx] import { fetchMetadata } from "frames.js/remix"; export async function loader({ request }) { diff --git a/docs/pages/reference/render/frame-ui.mdx b/docs/pages/reference/render/frame-ui.mdx index eb062c277..3611f3807 100644 --- a/docs/pages/reference/render/frame-ui.mdx +++ b/docs/pages/reference/render/frame-ui.mdx @@ -3,9 +3,9 @@ ## Usage ```tsx [frames.js/render/types.tsx] -import { FrameUI, fallbackFrameContext } from 'frames.js/render'; -import { FrameImageNext } from "frames.js/render/next"; -import { useFrame } from "frames.js/render/use-frame"; +import { FrameUI, fallbackFrameContext } from "@frames.js/render'; +import { FrameImageNext } from "@frames.js/render/next"; +import { useFrame } from "@frames.js/render/use-frame"; export const Page(){ @@ -35,4 +35,4 @@ const config = { "./node_modules/frames.js/dist/render/*.{ts,tsx,js,css}", "./node_modules/frames.js/dist/**/*.{ts,tsx,js,css}", ] -``` \ No newline at end of file +``` diff --git a/docs/pages/reference/render/next/frame-image.mdx b/docs/pages/reference/render/next/frame-image.mdx index 10141f77e..278daa50f 100644 --- a/docs/pages/reference/render/next/frame-image.mdx +++ b/docs/pages/reference/render/next/frame-image.mdx @@ -3,9 +3,9 @@ ## Usage ```tsx [frames.js/render/types.tsx] -import { FrameUI, fallbackFrameContext } from 'frames.js/render'; -import { useFrame } from 'frames.js/render/use-frame'; -import { FrameImageNext } from "frames.js/render/next"; +import { FrameUI, fallbackFrameContext } from "@frames.js/render'; +import { useFrame } from "@frames.js/render/use-frame'; +import { FrameImageNext } from "@frames.js/render/next"; export const Page(){ // ... @@ -18,4 +18,4 @@ export const Page(){ ); } -``` \ No newline at end of file +``` diff --git a/docs/pages/reference/render/next/get.mdx b/docs/pages/reference/render/next/get.mdx index bbf8b888d..b6dabd9fd 100644 --- a/docs/pages/reference/render/next/get.mdx +++ b/docs/pages/reference/render/next/get.mdx @@ -3,5 +3,5 @@ ## Usage ```tsx [./frames/route.tsx] -export { GET, POST } from 'frames.js/render/next'; -``` \ No newline at end of file +export { GET, POST } from "@frames.js/render/next'; +``` diff --git a/docs/pages/reference/render/next/post.mdx b/docs/pages/reference/render/next/post.mdx index 9c36324b2..d372a38fb 100644 --- a/docs/pages/reference/render/next/post.mdx +++ b/docs/pages/reference/render/next/post.mdx @@ -3,5 +3,5 @@ ## Usage ```tsx [./frames/route.tsx] -export { GET, POST } from 'frames.js/render/next'; -``` \ No newline at end of file +export { GET, POST } from "@frames.js/render/next'; +``` diff --git a/docs/pages/reference/render/types.mdx b/docs/pages/reference/render/types.mdx index bcb838fe1..14b3668f3 100644 --- a/docs/pages/reference/render/types.mdx +++ b/docs/pages/reference/render/types.mdx @@ -3,7 +3,7 @@ ## Example usage ```tsx -import { SignerStateInstance } from "frames.js/render"; +import { SignerStateInstance } from "@frames.js/render"; ``` ## Reference @@ -142,6 +142,4 @@ export type FrameTheme = Partial>; export interface FrameActionBodyPayload {} export type FrameContext = FarcasterFrameContext; - - -``` \ No newline at end of file +``` diff --git a/docs/pages/reference/render/use-frame.mdx b/docs/pages/reference/render/use-frame.mdx index 1e37aaee4..dc0115491 100644 --- a/docs/pages/reference/render/use-frame.mdx +++ b/docs/pages/reference/render/use-frame.mdx @@ -2,38 +2,154 @@ ## Props -```ts -type Props = { - /** the route used to POST frame actions. The post_url will be added as a the `url` query parameter */ - frameActionProxy: string; - /** the route used to GET the initial frame via proxy */ - frameGetProxy: string; - /** an auth state object used to determine what actions are possible */ - signerState: SignerStateInstance; - /** the url of the homeframe, if null won't load a frame */ - homeframeUrl: string | null; - /** the initial frame. if not specified will fetch it from the url prop */ - frame?: Frame; - /** a function to handle mint buttons */ - onMint?: (t: onMintArgs) => void; - /** the context of this frame, used for generating Frame Action payloads */ - frameContext: FrameContext; -} -``` +### `dangerousSkipSigning` + +- Type: `boolean` + +If true, the frame will not be signed before being sent to the frameActionProxy. This is useful for frames that don't verify signatures. + +### `frameActionProxy` + +- Type: `string` + +The route used to POST frame actions. The post_url will be added as a the `url` query parameter. + +### `frameGetProxy` + +- Type: `string` + +The route used to GET the initial frame via proxy. + +### `signerState` + +- Type: `SignerStateInstance` + +An signer state object used to determine what actions are possible. + +### `homeframeUrl` + +- Type: `string | null` + +The url of the homeframe, if null won't load a frame. + +### `frame` + +- Type: `Frame` + +The initial frame. if not specified will fetch it from the url prop. + +### `onMint` + +- Type: `(t: onMintArgs) => void` + +A function to handle mint buttons. + +### `onTransaction` + +- Type: `OnTransactionFunc` + +A function to handle transaction button presses, returns the transaction hash or null. + +### `frameContext` + +- Type: `FrameContext` + +The context of this frame, used for generating Frame Action payloads. + +### `extraButtonRequestPayload` + +- Type: `Record` + +Extra data appended to the frame action payload. + +## Returns + +- Type: `FrameState` + +### `fetchFrame` + +- Type: `(request: FrameRequest) => void` + +Fetches a frame from the frameGetProxy. + +### `clearFrameStack` + +- Type: `() => void` + +Clears the frame stack. + +### `frame` + +- Type: `Frame | null` + +The frame at the top of the stack (at index 0). + +### `framesStack` + +- Type: `FramesStack` + +A stack of frames with additional context, with the most recent frame at index 0. + +### `isLoading` + +- Type: `null | FrameStackPending` + +Whether the frame is loading. + +### `inputText` + +- Type: `string` + +The input text. + +### `setInputText` + +- Type: `(s: string) => void` + +Sets the input text. + +### `onButtonPress` + +- Type: `(frameButton: FrameButton, index: number) => void` + +Handles a button press. + +### `isFrameValid` + +- Type: `boolean | undefined` + +Whether the frame at the top of the stack has any frame validation errors. Undefined when the frame is not loaded or set. + +### `frameValidationErrors` + +- Type: `Record | undefined | null` + +The frame validation errors. + +### `error` + +- Type: `null | unknown` + +Whether there was an error loading the frame. + +### `homeframeUrl` + +- Type: `string | null` + +The url of the frame. ## Usage ```tsx [frames.js/render/types.tsx] "use client"; -import { - FrameUI, - fallbackFrameContext, - FrameContext, -} from "frames.js/render"; -import { FrameImageNext } from "frames.js/render/next"; +import { FrameUI, fallbackFrameContext, FrameContext } from "@frames.js/render"; +import { FrameImageNext } from "@frames.js/render/next"; import { FrameButton } from "frames.js"; -import { useFrame } from "frames.js/render/use-frame"; -import { mockFarcasterSigner, createFrameActionMessageWithSignerKey } from "frames.js/render/farcaster"; +import { useFrame } from "@frames.js/render/use-frame"; +import { + mockFarcasterSigner, + createFrameActionMessageWithSignerKey, +} from "@frames.js/render/farcaster"; export default function Page() { const frameState = useFrame({ @@ -54,7 +170,9 @@ export default function Page() { // Implement me alert("A frame button was pressed without a signer."); }, - signFrameAction: () => { alert('implement me.') }, + signFrameAction: () => { + alert("implement me."); + }, }, }); @@ -64,4 +182,4 @@ export default function Page() { } ``` -[Full example](/guides/display-frames.mdx) \ No newline at end of file +[Full example](/guides/apps/display-frames.mdx) diff --git a/docs/pages/troubleshooting.mdx b/docs/pages/troubleshooting.mdx new file mode 100644 index 000000000..89d1d05bc --- /dev/null +++ b/docs/pages/troubleshooting.mdx @@ -0,0 +1,139 @@ +# Troubleshooting + +## Image not rendering + +### Image too large + +If the image is not rendering, it may be too large. + +- If you are including a full size external `` element, consider passing the external image as a string instead: + +```tsx +return { + image: "https://example.com/image.jpg", +}; +``` + +- Try resizing it to a smaller size via the `imageOptions` of the returned `FrameDefinition`. + +```tsx +return { + image:
...
, + imageOptions: { + width: 100, + height: 100, + }, +}; +``` + +### Image wrong format + +If the image is not rendering, it may be in the wrong format. SVG images are typically not supported on mobile. + +## Initial frame not loading + +Ensure that the `fetchMetadata` URL is correct and that it is inside the `other` property for Next.js. + +### `VERCEL_URL` environment variable + +If you are using Vercel, the `VERCEL_URL` environment variable is not a fully qualified URL and may cause issues with `fetchMetadata`. You will have to prepend the protocol to `VERCEL_URL`. + +```tsx +export async function generateMetadata() { + const frameMetadata = await fetchMetadata( + new URL( + "/frames", + process.env.VERCEL_URL + ? `https://${process.env.VERCEL_URL}` + : "http://localhost:3000" + ) + ); + + return { + title: "My page", + other: { + ...frameMetadata, + }, + }; +} +``` + +### Vercel authentication + +When deploying to Vercel, your site will not automatically be accessible to the public. You will need to disable Vercel Authentication under your project's `Settings > Deployment Protection > Vercel Authentication` on vercel.com. + +## Import type errors + +If you are getting type errors when importing `frames.js`, you may need to change the `moduleResolution` in your `tsconfig.json` from `node` to `nodenext`. + +```json [tsconfig.json] +{ + "compilerOptions": { + "moduleResolution": "nodenext" + } +} +``` + +## Unable to access frame message on initial frame + +The initial frame is accessed via a GET request and does not have access to frame message and hence user data. + +## Type error: Route ... does not match the required types of a Next.js Route + +If you are getting this error you are exporting something other than a Next.js route from a `route.tsx` or `page.tsx` file. + +We recommend creating a new file for your `frames` app and importing it in the routes that use it. + +### Example + +Frames app file + +```tsx [frames.ts] +import { createFrames } from "frames.js/next"; + +export const frames = createFrames({ + basePath: "/frames", +}); +``` + +Initial page route + +```tsx [route.tsx] +import { frames } from "./frames"; + +export const GET = frames(async (ctx) => { + // ctx.message is not available in the initial frame + return { + image:
...
, + buttons: [ + , + ], + }; +}); +``` + +Frame action handler route + +```tsx [my-route/route.tsx] +import { frames } from "../frames"; + +export const POST = frames(async (ctx) => { + // Do something with ctx.message + // ... + + return { + image:
...
, + buttons: [ + // ... + ], + }; +}); +``` + +## Combining old and new SDKs + +You cannot use the ``, `` and `` components from the old SDK (`frames.js/next/server`) with the new SDK (`frames.js/next`). + +The new SDK uses a [`FrameDefinition`](/reference/core/createFrames#framedefinition) object to define a frame. diff --git a/docs/vocs.config.tsx b/docs/vocs.config.tsx index ee3842baf..6b0401df5 100644 --- a/docs/vocs.config.tsx +++ b/docs/vocs.config.tsx @@ -50,16 +50,22 @@ const sidebar = [ text: "Transactions", link: "/guides/transactions", }, - { - text: "Display Frames", - link: "/guides/display-frames", - }, { text: "Open Frames", link: "/guides/open-frames", }, ], }, + { + text: "Frames.js for Apps", + collapsed: false, + items: [ + { + text: "Display Frames", + link: "/guides/apps/display-frames", + }, + ], + }, { text: "Write your frame with", collapsed: false, @@ -100,6 +106,10 @@ const sidebar = [ }, ], }, + { + text: "Troubleshooting", + link: "/troubleshooting", + }, { text: "Reference", // link: "/reference", @@ -180,63 +190,7 @@ const sidebar = [ ], }, { - text: "frames.js/next/server", - collapsed: true, - items: [ - { - text: "getPreviousFrame", - link: "/reference/nextjs/getPreviousFrame", - }, - { - text: "POST", - link: "/reference/nextjs/POST", - }, - ], - }, - { - text: "frames.js/next/server - [react]", - collapsed: true, - items: [ - { - text: "types", - link: "/reference/react/types", - }, - { - text: "FrameContainer", - link: "/reference/react/FrameContainer", - }, - { - text: "FrameButton", - link: "/reference/react/FrameButton", - }, - { - text: "FrameImage", - link: "/reference/react/FrameImage", - }, - { - text: "FrameInput", - link: "/reference/react/FrameInput", - }, - { - text: "parseFrameParams", - link: "/reference/react/parseFrameParams", - }, - { - text: "useFramesReducer", - link: "/reference/react/useFramesReducer", - }, - { - text: "validateActionSignature", - link: "/reference/react/validateActionSignature", - }, - { - text: "createPreviousFrame", - link: "/reference/react/createPreviousFrame", - }, - ], - }, - { - text: "frames.js/render", + text: "@frames.js/render", collapsed: true, items: [ { @@ -251,23 +205,84 @@ const sidebar = [ text: "FrameUI", link: "/reference/render/frame-ui", }, + { + text: "Next.js", + collapsed: true, + items: [ + { + text: "FrameImage", + link: "/reference/render/next/frame-image", + }, + { + text: "POST", + link: "/reference/render/next/POST", + }, + { + text: "GET", + link: "/reference/render/next/GET", + }, + ], + }, ], }, { - text: "frames.js/render/next", - collapsed: true, + text: "Deprecated APIs", items: [ { - text: "FrameImage", - link: "/reference/render/next/frame-image", - }, - { - text: "POST", - link: "/reference/render/next/POST", + text: "frames.js/next/server", + collapsed: true, + items: [ + { + text: "getPreviousFrame", + link: "/reference/nextjs/getPreviousFrame", + }, + { + text: "POST", + link: "/reference/nextjs/POST", + }, + ], }, { - text: "GET", - link: "/reference/render/next/GET", + text: "frames.js/next/server - [react]", + collapsed: true, + items: [ + { + text: "types", + link: "/reference/react/types", + }, + { + text: "FrameContainer", + link: "/reference/react/FrameContainer", + }, + { + text: "FrameButton", + link: "/reference/react/FrameButton", + }, + { + text: "FrameImage", + link: "/reference/react/FrameImage", + }, + { + text: "FrameInput", + link: "/reference/react/FrameInput", + }, + { + text: "parseFrameParams", + link: "/reference/react/parseFrameParams", + }, + { + text: "useFramesReducer", + link: "/reference/react/useFramesReducer", + }, + { + text: "validateActionSignature", + link: "/reference/react/validateActionSignature", + }, + { + text: "createPreviousFrame", + link: "/reference/react/createPreviousFrame", + }, + ], }, ], },