Lens Support
Cross-protocol frames are supported by frames.js via familiar APIs. This guide will showcase how to write a simple stateless frame, which returns the Lens profileof the user that interacted with the frame in an OpenFrame supported application.
Setup
First, you need to install frames.js
and @lens-protocol/client
. You can do this by running the following command:
Writing a Frame
To write a frame using Next.js, we need to create a page which renders the frame and a route which handles frame action requests.
API Route
We start by creating a new API route which will handle POST requests to our frame. In your Next.js project, create a new directory /frames
and inside it, create a route.ts
file which contains the following code:
export { POST } from "frames.js/next/server";
This will handle POST requests to our frame and redirect them to page we are about to create, which will handle the rendering logic.
Page
In your Next.js project, create a new page.tsx
at the root of the project and write the following code:
First import the necessary functions and components from frames.js
:
import {
FrameButton,
FrameContainer,
FrameImage,
NextServerPageProps,
getFrameMessage,
getPreviousFrame,
} from "frames.js/next/server";
FrameButton
, FrameContainer
, and FrameImage
are components that are used to construct Frame metadata tags in HTML. NextServerPageProps
is a type that you can use to define the props of your page. getFrameMessage
and getPreviousFrame
are functions that you can use to get the message and the previous frame of your page to determine the next state to return.
Next, import the Lens validation methods from frames.js
import {
FrameButton,
FrameContainer,
FrameImage,
NextServerPageProps,
getFrameMessage,
getPreviousFrame,
} from "frames.js/next/server";
import { getLensFrameMessage, isLensFrameActionPayload } from "frames.js/lens";
Then define the client protocols that your frame will support. In our case we will support Lens, XMTP, and Farcaster.
import {
FrameButton,
FrameContainer,
FrameImage,
NextServerPageProps,
getFrameMessage,
getPreviousFrame,
} from "frames.js/next/server";
import { getLensFrameMessage, isLensFrameActionPayload } from "frames.js/lens";
const acceptedProtocols: ClientProtocolId[] = [
{
id: "lens",
version: "1.0.0",
},
{
id: "xmtp",
version: "vNext",
},
{
id: "farcaster",
version: "vNext",
},
];
Now define the render method for your frame. This will take place in a server component, so all the logic will be executed on the server and a plain HTML response containing our frame will be sent to the client.
// ...
export default async function Home({
params,
searchParams,
}: NextServerPageProps) {
const previousFrame = getPreviousFrame(searchParams);
// do some logic to determine the next frame
// return the frame
return (
<FrameContainer
pathname="/"
postUrl="/frames"
state={{}}
previousFrame={previousFrame}
accepts={acceptedProtocols}
>
<FrameImage>Hello world</FrameImage>
<FrameButton>Next</FrameButton>
</FrameContainer>
);
}
Here we use previousFrame
to extract the frame action payload and state from the previous frame which are stored in URL params. We then use these to determine the next frame to return, passing the new state and accepted protocols to the FrameContainer
component. The props of the FrameContainer
component determine the routing and functionality of the frame. pathname
is the path of the rendering method of the frame, postUrl
is the path of the API route that handles frame action requests, state
is the state of the frame, previousFrame
is the previous frame, and accepts
is an array of client protocols that this frame supports.
Validating a Frame Action
Before returning a frame or doing any processing, you may want to validate the frame and extract the context from which the frame was interacted with e.g. the user that clicked the button in a farcaster in an XMTP chat. This is where the getFrameMessage
, getXmtpFrameMessage
, and isXmtpFrameActionPayload
functions come in.
const previousFrame = getPreviousFrame(searchParams);
// do some logic to determine the next frame
let fid: number | undefined;
let walletAddress: string | undefined;
let lensProfileId: string | undefined;
if (
previousFrame.postBody &&
isLensFrameActionPayload(previousFrame.postBody)
) {
const frameMessage = await getLensFrameMessage(previousFrame.postBody);
// do something with Lens frame message
} else if (
previousFrame.postBody &&
isXmtpFrameActionPayload(previousFrame.postBody)
) {
const frameMessage = await getXmtpFrameMessage(previousFrame.postBody);
// do something with XMTP frame message
} else {
const frameMessage = await getFrameMessage(previousFrame.postBody);
// do something with Farcaster frame message
}
// ...
Here we use previousFrame.postBody
to extract the frame action payload from the previous frame. We then use isLensFrameActionPayload
to determine if the frame action payload is a Lens frame action payload. If it is, we use getLensFrameMessage
to extract the Lens frame message from the frame action payload. If it isn't, we fallback to methods from other supported clients.
Now we can use data from the different message contexts to populate our fid
and walletAddress
variables.
// ...
let fid: number | undefined;
let walletAddress: string | undefined;
let lensProfileId: string | undefined;
if (
previousFrame.postBody &&
isLensFrameActionPayload(previousFrame.postBody)
) {
const frameMessage = await getLensFrameMessage(previousFrame.postBody);
lensProfileId = frameMessage?.profileId;
} else if (
previousFrame.postBody &&
isXmtpFrameActionPayload(previousFrame.postBody)
) {
const frameMessage = await getXmtpFrameMessage(previousFrame.postBody);
walletAddress = frameMessage?.verifiedWalletAddress;
} else {
const frameMessage = await getFrameMessage(previousFrame.postBody);
if (frameMessage && frameMessage?.isValid) {
fid = frameMessage?.requesterFid;
walletAddress =
frameMessage?.requesterCustodyAddress.length > 0
? frameMessage?.requesterCustodyAddress
: frameMessage.requesterCustodyAddress;
}
}
// ...
Here we use the frameMessage
to extract the verifiedProfileId
, verifiedWalletAddress
and requesterFid
from the Lens, XMTP, and Farcaster frame messages respectively. We then use these to populate our lensProfileId
, walletAddress
and fid
variables. You can use this information to execute some action like a database query or an onchain transaction.
Returning a Frame
Now that we have our lensProfileId
, fid
and walletAddress
variables populated, we can use them to determine the next frame to return.
// ...
return (
<FrameContainer
pathname="/"
postUrl="/frames"
state={{}}
previousFrame={previousFrame}
accepts={acceptedProtocols}
>
<FrameImage>
<div tw="flex flex-col">
<div tw="flex">
This frame gets the interactor's wallet address or FID depending
on the client protocol.
</div>
{lensProfileId && <div tw="flex">Lens Profile ID: {lensProfileId}</div>}
{fid && <div tw="flex">FID: {fid}</div>}
{walletAddress && <div tw="flex">Wallet Address: {walletAddress}</div>}
</div>
</FrameImage>
<FrameButton>Check</FrameButton>
</FrameContainer>
);
Above, we conditionally render the lensProfileId
, fid
and walletAddress
variables in the frame.