Skip to content

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:
  1. Create a boilerplate Next.js app.
cmd
npx create-next-app my-next-app
  1. Install @coinbase/onchainkit as a dependency.
cmd
npm i @coinbase/onchainkit
  1. Add the base URL in .env.local as a NEXT_PUBLIC_BASE_URL environment variable.
  2. 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>
    </>
  );
}
  1. 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);
}
  1. 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`,
});
  1. 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);
}
  1. 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: