Build a subscribe Frame
Follow these steps to build a subscribe Open Frame that can be displayed in an app built with XMTP.
To build a subscribe Open Frame:- Create a boilerplate Next.js app.
cmd
npx create-next-app my-next-app
- Install
@coinbase/onchainkit
as a dependency.
cmd
npm i @coinbase/onchainkit
- Add the base URL in
.env.local
as aNEXT_PUBLIC_BASE_URL
environment variable. - In
app/page.tsx
, replace the boilerplate with the following code — this is what will be rendered as the initial frame:
import { getFrameMetadata } from "@coinbase/onchainkit/frame";
import { Metadata } from "next";
const frameMetadata = getFrameMetadata({
// Accepts and isOpenFrame keys are required for Open Frame compatibility
accepts: { xmtp: "2024-02-09" },
isOpenFrame: true,
buttons: [
{
// Whatever label you want your first button to have
label: "Subscribe to receive messages from this user!",
// Required 'tx' action for a transaction frame
action: "tx",
// Below buttons are 2 route urls that will be added in the next steps.
// Target will send back info about the subscribe frame
target: `${process.env.NEXT_PUBLIC_BASE_URL}/api/transaction`,
// postUrl will send back a subscription success screen
postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/transaction-success`,
},
],
// This is the image shown on the default screen
// Add whatever path is needed for your starting image
// In this case, using an Open Graph image
image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?subscribed=false`,
});
export const metadata: Metadata = {
title: "Subscribe Frame",
description: "A frame to demonstrate subscribing from a frame",
other: {
...frameMetadata,
},
};
export default function Home() {
return (
<>
<h1>Open Frames Subscribe Frame</h1>
</>
);
}
- Add the route to
/api/transaction/route.tsx
. The route is used to get information about the frame that is sent to the target URL.
import { NextRequest, NextResponse } from "next/server";
import { parseEther, encodeFunctionData } from "viem";
import type { FrameTransactionResponse } from "@coinbase/onchainkit/frame";
import { getXmtpFrameMessage } from "@coinbase/onchainkit/xmtp";
async function getResponse(req: NextRequest): Promise<NextResponse | Response> {
const body = await req.json();
const { isValid } = await getXmtpFrameMessage(body);
if (!isValid) {
return new NextResponse("Message not valid", { status: 500 });
}
const xmtpClient = // Your client instance; in the boilerplate frame, we're using a randomly generated wallet
const walletAddress = xmtpClient?.address || "";
const timestamp = Date.now();
// Store the timestamp however you'd like, in this case as an env variable, to cross-check at a later step.
process.env.TIMESTAMP = JSON.stringify(timestamp);
// Create the original consent message.
const message = createConsentMessage(walletAddress, timestamp);
const txData = {
// Sepolia or whichever chain id
chainId: `eip155:11155111`,
method: "eth_personalSign",
params: {
// This is the message the user will consent to, generated above
value: message
// These are required fields, but aren't utilized in this flow
abi: [],
to: walletAddress as `0x${string}`,
},
};
return NextResponse.json(txData);
}
export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}
- Get the confirmation frame screen HTML via the
@coinbase/onchainkit
helper to the success image and the success button action — in this case a redirect outside of the frame. (The redirect logic is outside the scope of this tutorial.) We recommend having a separate confirmation screen for users who subscribe and are not activated on XMTP, as they won't yet be able to receive messages.
const confirmationFrameHtmlWithXmtp = getFrameHtmlResponse({
accepts: {
xmtp: "2024-02-09",
},
isOpenFrame: true,
buttons: [
{
action: "post_redirect",
label: "Subscribed! Read more about Subscribe Frames",
},
],
postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/end`,
image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?subscribed=true&hasXmtp=true`,
});
const confirmationFrameHtmlNoXmtp = getFrameHtmlResponse({
accepts: {
xmtp: "2024-02-09",
},
isOpenFrame: true,
buttons: [
{
action: "post_redirect",
label: "Activate on XMTP to Receive Messages",
},
],
postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/endWithoutXmtp`,
image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?subscribed=true&hasXmtp=false`,
});
- Add the route to return the success frame HTML with the new meta tags at
api/transaction-success/route.ts
.
import { confirmationFrameHtml } from "@/app/page";
import { getXmtpFrameMessage } from "@coinbase/onchainkit/xmtp";
import { NextRequest, NextResponse } from "next/server";
import { createConsentProofPayload } from "@xmtp/consent-proof-signature";
async function getResponse(req: NextRequest): Promise<NextResponse> {
const body = await req.json();
const { isValid } = await getXmtpFrameMessage(body);
if (!isValid) {
return new NextResponse("Message not valid", { status: 500 });
}
const xmtpClient = // Your client
const signature = body.untrustedData.transactionId;
// Create the consent proof payload
const payloadBytes = createConsentProofPayload(signature, Date.now());
const consentProof = invitation.ConsentProofPayload.decode(
consentProofUint8Array
);
const payloadWithTimestamp = {
...consentProof,
timestamp: new Long(
consentProof?.timestamp?.low,
consentProof?.timestamp?.high,
consentProof?.timestamp?.unsigned
),
};
// Do whatever you want with the payload, in the below case we're immediately starting a new conversation
const newConvo = await xmtpClient?.conversations.newConversation(
body.untrustedData.address,
undefined,
payloadWithTimestamp
);
await newConvo?.send("Thank you for being a subscriber!");
// Determine if user is on XMTP or not and return the corresponding frame
const hasXmtp = await xmtpClient?.canMessage(body.untrustedData.address);
return new NextResponse(
hasXmtp ? confirmationFrameHtmlWithXmtp : confirmationFrameHtmlNoXmtp
);
}
export async function POST(req: NextRequest): Promise<Response> {
return getResponse(req);
}
- Send your subscription Frame in an XMTP message and try interacting with it!
Resources
If you need an XMTP messaging app to use, try one of these: