diff --git a/examples/gm/src/commands.ts b/examples/gm/src/commands.ts new file mode 100644 index 00000000..27f0c327 --- /dev/null +++ b/examples/gm/src/commands.ts @@ -0,0 +1,13 @@ +export const commands = [ + { + name: "General Commands", + icon: "🔧", + description: "Command for managing default behaviours.", + commands: [ + { + command: "/help", + description: "Get help with the bot.", + }, + ], + }, +]; diff --git a/packages/docs/docs/pages/commands.mdx b/packages/docs/docs/pages/commands.mdx new file mode 100644 index 00000000..9283a49d --- /dev/null +++ b/packages/docs/docs/pages/commands.mdx @@ -0,0 +1,37 @@ +# Commands + +Each bot can have a `commands.ts` file that contains the commands that the bot can execute. The commands are defined as an array of objects with the following keys: + +```tsx +export const commands = [ + { + name: "General Commands", + icon: "🔧", + description: "Command for managing default behaviours.", + commands: [ + { + command: "/help", + description: "Get help with the bot.", + }, + ], + }, +]; +``` + +### Declare commands + +To declare the access command in the botkit app, you need to add the following code: + +```tsx +import { commands } from "./commands.js"; + +const newBotConfig = { + context: { + commands: commands, + }, +}; + +run(async (context: HandlerContext) => { + //Your logic here +}, newBotConfig); +``` diff --git a/packages/docs/docs/pages/open-frames/Framesjs.mdx b/packages/docs/docs/pages/open-frames/frameworks/Framesjs.mdx similarity index 100% rename from packages/docs/docs/pages/open-frames/Framesjs.mdx rename to packages/docs/docs/pages/open-frames/frameworks/Framesjs.mdx diff --git a/packages/docs/docs/pages/open-frames/Frog.mdx b/packages/docs/docs/pages/open-frames/frameworks/Frog.mdx similarity index 100% rename from packages/docs/docs/pages/open-frames/Frog.mdx rename to packages/docs/docs/pages/open-frames/frameworks/Frog.mdx diff --git a/packages/docs/docs/pages/open-frames/OnchainKit.mdx b/packages/docs/docs/pages/open-frames/frameworks/OnchainKit.mdx similarity index 100% rename from packages/docs/docs/pages/open-frames/OnchainKit.mdx rename to packages/docs/docs/pages/open-frames/frameworks/OnchainKit.mdx diff --git a/packages/docs/docs/pages/open-frames/tutorials/subscribe.mdx b/packages/docs/docs/pages/open-frames/tutorials/subscribe.mdx new file mode 100644 index 00000000..c2e7760b --- /dev/null +++ b/packages/docs/docs/pages/open-frames/tutorials/subscribe.mdx @@ -0,0 +1,320 @@ +# Subscribe in Open Frames + +### Sections: + +1. **How to render a subscribe Open Frame**: Learn the steps to display subscribe frames within your application. +2. **Building a subscribe frame**: Implement the method to construct and manage subscribe frames. + +--- + +## Support a subscribe Open Frame in an app built with XMTP + +In this guide, we will walk through the process of supporting subscribe Open Frames in your application using XMTP. Each section provides step-by-step instructions to integrate this functionality. + +### Determine if an Open Frame is a subscribe frame + +Subscriptions via a frame are triggered using button-click events and are a type of transactional frame. Therefore, the best way to determine if an Open Frame is a subscribe frame begins with the same first step as a typical transactional frame: looking for a button `action` set to `tx` in the frame metadata. For example: + +```jsx +import { OpenFramesProxy } from "@xmtp/frames-client"; + +const proxy = new OpenFramesProxy(); +const frameMetadata = proxy.readMetadata("url_of_frame"); + +// Get correct button index from click handler +const button = frameMetadata.frameInfo.buttons[`${buttonIndex}`]; + +const isTransactionFrame = button.action === "tx"; +``` + +### Determine the transaction target and post URL + +If the button action indicates the Frame is transactional, get the `target` and `postUrl` from the button. To learn more, see Frame Metadata [Optional Properties](https://www.openframes.xyz/#optional-properties). + +```jsx +if (isTransactionFrame) { + const { target, postUrl } = button; + + // Rest of logic in this guide +} +``` + +### Post to the target URL to fetch data + +This next step is also the same as a regular transaction frame: make a POST request to the `target` URL to fetch transaction data. The difference is that the payload that gets returned will return an eth_personalSign method if in the subscribe flow, and this is how you know you're dealing with a subscription frame. + +Make this request from the Frame with a signed Frame action payload in the POST body. In the `address` field, include the address of the connected wallet. + +```jsx +import { FramesClient } from "@xmtp/frames-client"; + +const framesClient = new FramesClient(client); + + const payload = await framesClient.signFrameAction({ + // Same payload as for other frames, + an address field + // Address should be the 0x address of the connected account + address, + }); + +const transactionInfo: { + chainId: string; + method: 'eth_personalSign'; + params: { + abi: []; + to: `0x${string}`; + value?: string; // In the case of a subscribe frame, this will be the message that the user will consent to + }; +} = await framesClient.proxy.postTransaction( + target, + payload, + ); +``` + +### Process transaction data and receive a signature + +Pull the consent message from the value of the returned transaction data and use this to get a signature using your preferred tools, such as Viem. Documenting this step in detail is out of the scope of this tutorial. + +```jsx +const value = transactionInfo.params.value; + +// Pass the value and account to your preferred tools and receive a signature +const signature = <> +``` + +### Pass this signature to the signFrameAction + +Prepare a new payload with the signature passed as transactionId to pass to the final step. + +```jsx +const payload = await framesClient.signFrameAction({ + ...prevPayload, + transactionId: signature, +}); +``` + +### Complete the subscription and show success screen + +Pass the postUrl from the button as well as the payload with the signature from the previous step to complete the subscription flow and return metadata of a new success frame. + +```jsx +const completeTransaction = await framesClient.proxy.post( + buttonPostUrl, + payloadWithTxId, +); +// Finally, set the current frame state to this new metadata/success screen +``` + +### Try an example subscribe Open Frame + +Use the example [Open Frames Subscribe Frame](https://subscribe-boilerplate-frame.vercel.app/) to try these steps out in your app. Or check the code of the [open source repo](https://github.com/xmtp-labs/subscribe-boilerplate-frame). + +This example Frame uses a randomly generated wallet in the XMTP dev network to automatically send a "Thank you for subscribing!" message to your main inbox upon subscribing. + +--- + +## Build a subscribe Open 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. + +```bash +npx create-next-app my-next-app +``` + +2. Install `@coinbase/onchainkit` as a dependency. + +```bash +npm install @coinbase/onchainkit +``` + +3. Add the base URL in `.env.local` as a `NEXT_PUBLIC_BASE_URL` environment variable. +4. In `app/page.tsx`, replace the boilerplate with the following code — this is what will be rendered as the initial frame: + +```jsx +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 ( + <> +

Open Frames Subscribe Frame

+ + ); +} +``` + +5. 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. + +```jsx +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 { + 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 { + return getResponse(req); +} +``` + +6. 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. + +```jsx +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`, +}); +``` + +7. Add the route to return the success frame HTML with the new meta tags at `api/transaction-success/route.ts`. + +```jsx +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 { + 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 { + return getResponse(req); +} +``` + +8. 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: + +- https://app-preview.converse.xyz/ +- https://dev-dev-inbox.vercel.app/ diff --git a/packages/docs/docs/pages/open-frames/tutorials/transactions.mdx b/packages/docs/docs/pages/open-frames/tutorials/transactions.mdx new file mode 100644 index 00000000..37526660 --- /dev/null +++ b/packages/docs/docs/pages/open-frames/tutorials/transactions.mdx @@ -0,0 +1,312 @@ +# Transactions and Mints in Open Frames + +Note: A mint is a form of a transaction frame. Where applicable, steps for mint frames will be specified through this tutorial. + +### Sections: + +1. **How to render a transactional Open Frame**: Learn the steps to display transactional frames within your application. +2. **Security considerations for transactional frames**: Understand the security measures needed when dealing with transactional frames. +3. **Building a transactional frame**: Implement the method to construct and manage transactional frames. + +--- + +## Support a transactional Open Frame in an app built with XMTP + +In this guide, we will walk through the process of supporting transactional Open Frames in your application using XMTP. Each section provides step-by-step instructions to integrate this functionality. + +### Determine if an Open Frame is transactional + +Frame transactions are triggered using button-click events. + +Therefore, the best way to determine if an Open Frame is transactional is to look for a button `action` set to `tx` in the frame metadata. For example: + +```jsx +import { OpenFramesProxy } from "@xmtp/frames-client"; + +const proxy = new OpenFramesProxy(); +const frameMetadata = proxy.readMetadata("url_of_frame"); + +// Get correct button index from click handler +const button = frameMetadata.frameInfo.buttons[`${buttonIndex}`]; + +const isTransactionFrame = button.action === "tx"; +``` + +### Determine the transaction target and post URL + +If the button action indicates the Frame is transactional, get the `target` and `postUrl` from the button. To learn more, see Frame Metadata [Optional Properties](https://www.openframes.xyz/#optional-properties). + +```jsx +if (isTransactionFrame) { + const { target, postUrl } = button; + + // Rest of logic in this guide +} +``` + +### Post to the target URL to fetch transaction data + +Make a POST request to the `target` URL to fetch transaction data. + +Make this request from the Frame with a signed Frame action payload in the POST body. In the `address` field, include the address of the connected wallet. + +```jsx +import { FramesClient } from "@xmtp/frames-client"; + +const framesClient = new FramesClient(client); + + const payload = await framesClient.signFrameAction({ + // Same payload as for other frames, + an address field + // Address should be the 0x address of the connected account + address, + }); + +const transactionInfo: { + chainId: string; + method: 'eth_sendTransaction'; + params: { + abi: Abi | []; + to: `0x${string}`; + value?: string; + // Needed if you are interacting with a smart contract in this transaction, e.g. in a mint scenario + data?: `0x${string}`; + }; +} = await framesClient.proxy.postTransaction( + target, + payload, + ); +``` + +### Process transaction data and receive a hash + +Pull the address and value from the returned transaction data and use them to process the transaction using your preferred tools, such as Infura. Documenting this step in detail is out of the scope of this tutorial. + +```jsx +const address = transactionInfo.params.to; +// Returned as wei in a string +const value = Number(transactionInfo.params.value); + + +// Pass the address, value, and any other information needed +// Process the payment via your preferred tools and receive a hash. +const transactionHash = <> +``` + +### Ensure the processed transaction matches the request + +Use the hash to gather information about the processed transaction using your preferred tools. Ensure that the details match the requested transaction. + +```jsx +// Pass the hash to your provider of choice +// Receive the processed transaction details +const transactionReceipt = <> + +if ( + transactionReceipt.to !== address || transactionReceipt.value !== value + ) { + // Error handle, shouldn't show frame success screen + } else { + // Pass the hash as an optional transactionId to the signFrameAction payload if you plan to use it + // Complete the transaction, which returns metadata of a new success frame + const completeTransaction = await framesClient.proxy.post( + postUrl, + payload, + ); + // Set the current frame state to this new metadata/success screen + } +} +``` + +### Try an example transaction Open Frame + +Use the example [Open Frames Tx Frame](https://tx-boilerplate-frame.vercel.app/) to try these steps out in your app. Or check the code of the [open source repo](https://github.com/xmtp-labs/tx-boilerplate-frame). + +This example Frame uses the Sepolia network to make a 0.0000032ETH (~1 cent) transaction to the address associated with hi.xmtp.eth. + +### Try an example mint Open Frame with a transaction + +Use the example [Open Frames Mint Tx Frame](https://mint-tx-boilerplate-frame.vercel.app/) to try these steps out in your app. Or check the code of the [open source repo](https://github.com/xmtp-labs/mint-tx-boilerplate-frame). + +This example Frame uses the Sepolia network to make a 0.0000032ETH (~1 cent) transaction and mint an NFT of an AI dog. + +--- + +## Security considerations + +When rendering transaction Frames in your app, consider providing these security best practices to keep your users safe: + +- Include allow lists that enable your app to interact with known “safe” transaction frames only +- For unknown frames, inform the user that they are about to interact with an unknown Frame and to proceed at their own risk. +- Use simulation services in cases where you want to allow access to unverified transaction Frames. These services enable you to submit transaction information to a simulator first, which enables you to test the process without financial risk and retrieve debit amount details. +- Apps rendering transaction Frames should avoid the `mainnet` for now. The associated fees can be high for most of the current use cases for transaction Frames. + +For more transaction Frame security considerations as well as mitigation strategies, see the [Farcaster transaction Frame security documentation](https://www.notion.so/Frame-Transactions-Public-9d9f9f4f527249519a41bd8d16165f73?pvs=21). + +--- + +## Build a transaction Open Frame + +Follow these steps to build a transaction Open Frame that can be displayed in an app built with XMTP. + +**To build a transaction Open Frame:** + +1. Create a boilerplate Next.js app. + +```bash +npx create-next-app my-next-app +``` + +2. Install `@coinbase/onchainkit` as a dependency. + +```bash +npm install @coinbase/onchainkit +``` + +3. Add the base URL in `.env.local` as a `NEXT_PUBLIC_BASE_URL` environment variable. +4. In `app/page.tsx`, replace the boilerplate with the following code — this is what will be rendered as the initial frame: + +```jsx +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: "Make transaction", + // 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 transaction + target: `${process.env.NEXT_PUBLIC_BASE_URL}/api/transaction`, + // postUrl will send back a transaction 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?transaction=null`, +}); + +export const metadata: Metadata = { + title: "Transaction Frame", + description: "A frame to demonstrate transactions", + other: { + ...frameMetadata, + }, +}; + +export default function Home() { + return ( + <> +

Open Frames Tx Frame

+ + ); +} +``` + +5. Add the route to `/api/transaction/route.tsx`. The route is used to get information about the transaction that is sent to the target URL. + +```jsx +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 { + const body = await req.json(); + const { isValid } = await getXmtpFrameMessage(body); + if (!isValid) { + return new NextResponse("Message not valid", { status: 500 }); + } + + // This optional param is needed in scenarios where you're interacting with a smart contract + // The values passed will depend on the implementation details of your contract; this is just an example + const data = encodeFunctionData({ + abi: JSON.parse(contractAbi), + functionName: "publicMint", + args: [], + }); + + const txData: FrameTransactionResponse = { + // Sepolia or whichever chain id; we suggest avoiding mainnet for now + chainId: `eip155:11155111`, + method: "eth_sendTransaction", + params: { + abi: [], + // Address receiving the transaction — in this case, hi.xmtp.eth + to: "0x194c31cAe1418D5256E8c58e0d08Aee1046C6Ed0", + // Transaction value in eth sent back as wei — in this case, ~1 cent. + value: parseEther("0.0000032", "wei").toString(), + data, // If applicable + }, + }; + return NextResponse.json(txData); +} + +export async function POST(req: NextRequest): Promise { + return getResponse(req); +} +``` + +6. 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.) + +```jsx +export const confirmationFrameHtml = getFrameHtmlResponse({ + accepts: { + xmtp: "2024-02-09", + }, + isOpenFrame: true, + buttons: [ + { + action: "post_redirect", + label: "Learn more about transaction frames", + }, + ], + postUrl: `${process.env.NEXT_PUBLIC_BASE_URL}/api/end`, + image: `${process.env.NEXT_PUBLIC_BASE_URL}/api/og?transaction=0.0000032`, +}); +``` + +7. Add the route to return the success frame HTML with the new meta tags at `api/transaction-success/route.ts`. + +```jsx +import { confirmationFrameHtml } from "@/app/page"; +import { getXmtpFrameMessage } from "@coinbase/onchainkit/xmtp"; +import { NextRequest, NextResponse } from "next/server"; + +async function getResponse(req: NextRequest): Promise { + const body = await req.json(); + const { isValid } = await getXmtpFrameMessage(body); + + if (!isValid) { + return new NextResponse("Message not valid", { status: 500 }); + } + + return new NextResponse(confirmationFrameHtml); +} +export async function POST(req: NextRequest): Promise { + return getResponse(req); +} +``` + +8. Send your transaction Frame in an XMTP message and try interacting with it! + +:::info + +🧪 If you’re using a boilerplate Frame we just built, be sure you’re on the `Sepolia` network. + +::: + +### Resources + +If you need an XMTP messaging app to use, try one of these: + +- https://app-preview.converse.xyz/ +- https://dev-dev-inbox.vercel.app/ diff --git a/packages/docs/docs/pages/structure.mdx b/packages/docs/docs/pages/structure.mdx index dd07a302..69ad38d8 100644 --- a/packages/docs/docs/pages/structure.mdx +++ b/packages/docs/docs/pages/structure.mdx @@ -35,15 +35,24 @@ run(async (context: HandlerContext) => { Each bot can have a `commands.ts` file that contains the commands that the bot can execute. The commands are defined as an array of objects with the following keys: -```json +```tsx export const commands = [ { - command: "/help", - description: "Get help with the bot.", + name: "General Commands", + icon: "🔧", + description: "Command for managing default behaviours.", + commands: [ + { + command: "/help", + description: "Get help with the bot.", + }, + ], }, ]; ``` +To learn more go to [commands](./commands) + ### Environment variables Each bot should have an `.env` file that contains the following: diff --git a/packages/docs/vocs.config.ts b/packages/docs/vocs.config.ts index cf50fee0..1bdddba4 100644 --- a/packages/docs/vocs.config.ts +++ b/packages/docs/vocs.config.ts @@ -29,13 +29,17 @@ export default defineConfig({ link: "/structure", }, { - text: "Examples", - link: "/examples", + text: "Commands", + link: "/commands", }, { text: "Access", link: "/access", }, + { + text: "Examples", + link: "/examples", + }, { text: "Content Types", items: [ @@ -83,7 +87,7 @@ export default defineConfig({ link: "/middleware/cron", }, { - text: "Commands", + text: "Parse commands", link: "/middleware/commands", }, ], @@ -93,16 +97,38 @@ export default defineConfig({ link: "/open-frames", items: [ { - text: "OnchainKit", - link: "/open-frames/onchainkit", + text: "Introduction", + link: "/open-frames", }, { - text: "Frames.js", - link: "/open-frames/framesjs", + text: "Frameworks", + items: [ + { + text: "OnchainKit", + link: "/open-frames/frameworks/onchainkit", + }, + { + text: "Frames.js", + link: "/open-frames/frameworks/framesjs", + }, + { + text: "Frog", + link: "/open-frames/frameworks/frog", + }, + ], }, { - text: "Frog", - link: "/open-frames/frog", + text: "Tutorials", + items: [ + { + text: "Subscribe", + link: "/open-frames/tutorials/subscribe", + }, + { + text: "Transactions", + link: "/open-frames/tutorials/transactions", + }, + ], }, ], }, diff --git a/packages/playground/src/Groups/MessageContainer.js b/packages/playground/src/Groups/MessageContainer.js index 7bde7d5e..e9286164 100644 --- a/packages/playground/src/Groups/MessageContainer.js +++ b/packages/playground/src/Groups/MessageContainer.js @@ -363,10 +363,11 @@ export const MessageContainer = ({ const handleSetTextInputValue = (value) => { setTextInputValue(value); }; - const [hasAccess, setHasAccess] = useState(false); + const [hasAccess, setHasAccess] = useState(true); useEffect(() => { if (commands.length > 0 && !hasAccess) { + console.log(commands, hasAccess); // Assuming that the presence of "/access" means access needs to be requested or granted const accessRequired = commands.some((command) => command.commands?.some((item) => item.command === "/access"),