{"id":31644482,"url":"https://github.com/10xmeme/miniapp","last_synced_at":"2025-10-07T04:53:16.809Z","repository":{"id":316380638,"uuid":"1063069045","full_name":"10XMeme/miniapp","owner":"10XMeme","description":"10X.MEME Mini App","archived":false,"fork":false,"pushed_at":"2025-09-24T08:01:09.000Z","size":3405,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2025-09-24T09:43:40.369Z","etag":null,"topics":["base","crypto","farcaster","farcaster-frames","memes"],"latest_commit_sha":null,"homepage":"https://app.10x.meme","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/10XMeme.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-09-24T06:02:59.000Z","updated_at":"2025-09-24T08:03:53.000Z","dependencies_parsed_at":"2025-09-24T09:45:08.715Z","dependency_job_id":null,"html_url":"https://github.com/10XMeme/miniapp","commit_stats":null,"previous_names":["10xmeme/miniapp"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/10XMeme/miniapp","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/10XMeme%2Fminiapp","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/10XMeme%2Fminiapp/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/10XMeme%2Fminiapp/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/10XMeme%2Fminiapp/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/10XMeme","download_url":"https://codeload.github.com/10XMeme/miniapp/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/10XMeme%2Fminiapp/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278722768,"owners_count":26034461,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["base","crypto","farcaster","farcaster-frames","memes"],"created_at":"2025-10-07T04:53:15.043Z","updated_at":"2025-10-07T04:53:16.804Z","avatar_url":"https://github.com/10XMeme.png","language":"TypeScript","readme":"# 💎 10X.MEME Mini App\n\n[👉 APP.10X.MEME](https://app.10x.meme)\n\n\u003e [!NOTE]\n\u003e Forked from Farcaster Frames v2 demo app: https://github.com/farcasterxyz/frames-v2-demo\n\n# 🖼️ frames-v2-demo\n\nA Farcaster Frames v2 demo app.\n\n[🛠️ Frame Playground](https://warpcast.com/~/developers/frame-playground) (Mobile only)\u003cbr/\u003e\n[📦 Frame SDK](https://github.com/farcasterxyz/frames/)\u003cbr/\u003e\n[👀 Dev preview docs](https://github.com/farcasterxyz/frames/wiki/frames-v2-developer-playground-preview)\u003cbr/\u003e\n\n## Getting Started\n\nThis is a [NextJS](https://nextjs.org/) + TypeScript + React app.\n\nTo install dependencies:\n\n```bash\n$ pnpm\n```\n\nTo run the app:\n\n```bash\n$ pnpm dev\n```\n\nTo try your app in the Warpcast playground, you'll want to use a tunneling tool like [ngrok](https://ngrok.com/).\n\n## Tutorial\n\nHere's a full walkthrough of creating a frames v2 app:\n\n[![Frames v2 Tutorial](https://img.youtube.com/vi/5wAbo_YsuC4/0.jpg)](https://www.youtube.com/watch?v=5wAbo_YsuC4)\n\n[📺 View video](https://www.youtube.com/watch?v=5wAbo_YsuC4)\n\n### Setup and dependencies\n\nWe'll start with a fresh NextJS app:\n\n```bash\n$ pnpm create next-app\n✔ What is your project named? … frames-v2-demo\n✔ Would you like to use TypeScript? … No / Yes\n✔ Would you like to use ESLint? … No / Yes\n✔ Would you like to use Tailwind CSS? … No / Yes\n✔ Would you like your code inside a `src/` directory? … No / Yes\n✔ Would you like to use App Router? (recommended) … No / Yes\n✔ Would you like to use Turbopack for next dev? … No / Yes\n✔ Would you like to customize the import alias (@/* by default)? … No / Yes\n✔ What import alias would you like configured? … ~/*\nCreating a new Next.js app in /Users/horsefacts/Projects/frames-v2-demo.\n```\n\nNext, install frame related dependencies. We'll need the official frame SDK:\n\n```bash\n$ pnpm add @farcaster/frame-sdk\n```\n\nWe'll also need [Wagmi](https://wagmi.sh/) to handle wallet interactions. Let's install it and its dependencies.\n\n```bash\n$ pnpm add wagmi viem@2.x @tanstack/react-query\n```\n\nOK, we're ready to get started!\n\n### Configuring providers\n\nWe'll need to set up a custom Wagmi connector in order to interact with the user's Farcaster wallet. Since the frames SDK is a frontend only package, we'll also need to use client components and [Next dynamic imports](https://nextjs.org/docs/pages/building-your-application/optimizing/lazy-loading#nextdynamic) in a few places.\n\nFirst, let's create a custom connector component at `lib/connector.ts`. We'll use this to connect to the user's Farcaster wallet from our app.\n\n\u003e [!NOTE]\n\u003e We plan to move this connector into the frames SDK so you don't have to worry about it. But you'll need to copy-paste it for now.\n\n```ts\nimport sdk from \"@farcaster/frame-sdk\";\nimport { SwitchChainError, fromHex, getAddress, numberToHex } from \"viem\";\nimport { ChainNotConfiguredError, createConnector } from \"wagmi\";\n\nframeConnector.type = \"frameConnector\" as const;\n\nexport function frameConnector() {\n  let connected = true;\n\n  return createConnector\u003ctypeof sdk.wallet.ethProvider\u003e((config) =\u003e ({\n    id: \"farcaster\",\n    name: \"Farcaster Wallet\",\n    type: frameConnector.type,\n\n    async setup() {\n      this.connect({ chainId: config.chains[0].id });\n    },\n    async connect({ chainId } = {}) {\n      const provider = await this.getProvider();\n      const accounts = await provider.request({\n        method: \"eth_requestAccounts\",\n      });\n\n      let currentChainId = await this.getChainId();\n      if (chainId \u0026\u0026 currentChainId !== chainId) {\n        const chain = await this.switchChain!({ chainId });\n        currentChainId = chain.id;\n      }\n\n      connected = true;\n\n      return {\n        accounts: accounts.map((x) =\u003e getAddress(x)),\n        chainId: currentChainId,\n      };\n    },\n    async disconnect() {\n      connected = false;\n    },\n    async getAccounts() {\n      if (!connected) throw new Error(\"Not connected\");\n      const provider = await this.getProvider();\n      const accounts = await provider.request({\n        method: \"eth_requestAccounts\",\n      });\n      return accounts.map((x) =\u003e getAddress(x));\n    },\n    async getChainId() {\n      const provider = await this.getProvider();\n      const hexChainId = await provider.request({ method: \"eth_chainId\" });\n      return fromHex(hexChainId, \"number\");\n    },\n    async isAuthorized() {\n      if (!connected) {\n        return false;\n      }\n\n      const accounts = await this.getAccounts();\n      return !!accounts.length;\n    },\n    async switchChain({ chainId }) {\n      const provider = await this.getProvider();\n      const chain = config.chains.find((x) =\u003e x.id === chainId);\n      if (!chain) throw new SwitchChainError(new ChainNotConfiguredError());\n\n      await provider.request({\n        method: \"wallet_switchEthereumChain\",\n        params: [{ chainId: numberToHex(chainId) }],\n      });\n      return chain;\n    },\n    onAccountsChanged(accounts) {\n      if (accounts.length === 0) this.onDisconnect();\n      else\n        config.emitter.emit(\"change\", {\n          accounts: accounts.map((x) =\u003e getAddress(x)),\n        });\n    },\n    onChainChanged(chain) {\n      const chainId = Number(chain);\n      config.emitter.emit(\"change\", { chainId });\n    },\n    async onDisconnect() {\n      config.emitter.emit(\"disconnect\");\n      connected = false;\n    },\n    async getProvider() {\n      return sdk.wallet.ethProvider;\n    },\n  }));\n}\n```\n\nNext, let's create a provider component that handles our Wagmi configuration. Create `components/providers/WagmiProvider.tsx`.\n\nWe'll configure our client with Base as a connected network and use the `frameConnector` that we just created:\n\n```\nimport { createConfig, http, WagmiProvider } from \"wagmi\";\nimport { base } from \"wagmi/chains\";\nimport { QueryClient, QueryClientProvider } from \"@tanstack/react-query\";\nimport { frameConnector } from \"~/lib/connector\";\n\nexport const config = createConfig({\n  chains: [base],\n  transports: {\n    [base.id]: http(),\n  },\n  connectors: [frameConnector()],\n});\n\nconst queryClient = new QueryClient();\n\nexport default function Provider({ children }: { children: React.ReactNode }) {\n  return (\n    \u003cWagmiProvider config={config}\u003e\n      \u003cQueryClientProvider client={queryClient}\u003e{children}\u003c/QueryClientProvider\u003e\n    \u003c/WagmiProvider\u003e\n  );\n}\n```\n\nNow let's create a top-level `Providers` component that will include all our required providers. In this simple demo app, we'll just be adding Wagmi, but this is where you might also add other providers necessary for your own app.\n\nCreate `app/providers.tsx`:\n\n```tsx\n\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nconst WagmiProvider = dynamic(\n  () =\u003e import(\"~/components/providers/WagmiProvider\"),\n  {\n    ssr: false,\n  }\n);\n\nexport function Providers({ children }: { children: React.ReactNode }) {\n  return \u003cWagmiProvider\u003e{children}\u003c/WagmiProvider\u003e;\n}\n```\n\nNote two new things here: since the SDK relies on the browser `window`, we need to define this as a client component with `\"use client\";` and use a dynamic import to import `WagmiProvider`.\n\nFinally, let's add this providers component to our app layout. Edit `app/layout.tsx`:\n\n```tsx\nimport type { Metadata } from \"next\";\n\nimport \"~/app/globals.css\";\nimport { Providers } from \"~/app/providers\";\n\nexport const metadata: Metadata = {\n  title: \"Farcaster Frames v2 Demo\",\n  description: \"A Farcaster Frames v2 demo app\",\n};\n\nexport default function RootLayout({\n  children,\n}: Readonly\u003c{\n  children: React.ReactNode;\n}\u003e) {\n  return (\n    \u003chtml lang=\"en\"\u003e\n      \u003cbody\u003e\n        \u003cProviders\u003e{children}\u003c/Providers\u003e\n      \u003c/body\u003e\n    \u003c/html\u003e\n  );\n}\n```\n\nOK, setup is all done, let's do something more interesting...\n\n### Creating the app\n\nLet's create a component for our app's `homeUrl` page. Create `app/components/Demo.tsx`.\n\nFor now, let's just put in a placeholder, Since our frame app will be rendering at mobile width, we'll give it a fixed width and center the content:\n\n```tsx\nexport default function Demo() {\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nSince we're going to import the frames SDK in this component, we'll need to load it dynamically, too. Edit `app/page.tsx`:\n\n```tsx\n\"use client\";\n\nimport dynamic from \"next/dynamic\";\n\nconst Demo = dynamic(() =\u003e import(\"~/components/Demo\"), {\n  ssr: false,\n});\n\nexport default function Home() {\n  return (\n    \u003cmain className=\"min-h-screen flex flex-col p-4\"\u003e\n      \u003cDemo /\u003e\n    \u003c/main\u003e\n  );\n}\n```\n\nOK, we're all set up! Now is a good time to try out our frames app in the developer playground. To do so, we'll use ngrok to access our local dev server over the internet.\n\nFirst, run the dev server:\n\n```bash\n$ pnpm dev\n```\n\nNext, start ngrok:\n\n```bash\n$ ngrok http http://localhost:3000\n```\n\nNow open the Frame Playground on Warpcast mobile, by visiting [https://warpcast.com/~/developers/frame-playground](https://warpcast.com/~/developers/frame-playground).\n\nEnter your ngrok URL:\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/1_playground.png\" width=\"200\" alt=\"Frames Playground\" /\u003e\n\n..and tap \"Launch\" to open your app.\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/2_blank.png\" width=\"200\" alt=\"Launch\" /\u003e\n\nIf you watch your dev server and ngrok logs, you'll see a request to your server. But nothing will load until we signal to Warpcast that our app is `ready()`.\n\n### Calling `ready()`\n\nTo give frames a consistent loading experience, clients display a splash screen and image until the app calls `sdk.actions.ready()`. In order to make it more visible here, let's add a splash image and loading color:\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/3_config.png\" width=\"200\" alt=\"Config\" /\u003e\n\nNow we get a nice background color and splash image:\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/4_splash.png\" width=\"200\" alt=\"Splash\" /\u003e\n\nLet's call `ready()` to load our app. We'll call `sdk.actions.ready()` in an effect on render, which tells the parent Farcaster app that our frame is ready to render and hides the splash screen:\n\n```tsx\nimport { useEffect, useState } from \"react\";\nimport sdk from \"@farcaster/frame-sdk\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nTry again in the playground and we'll see our app:\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/5_hello.png\" width=\"200\" alt=\"Hello\" /\u003e\n\n### Viewing context\n\nWhen your frame loads, the parent Farcaster app provides it with context information, including the current user. Let's take a look at it.\n\nWe can access the context data at `sdk.context` to see information about the current user.:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      \u003cdiv className=\"mb-4\"\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eContext\u003c/h2\u003e\n        \u003cbutton\n          onClick={toggleContext}\n          className=\"flex items-center gap-2 transition-colors\"\n        \u003e\n          \u003cspan\n            className={`transform transition-transform ${\n              isContextOpen ? \"rotate-90\" : \"\"\n            }`}\n          \u003e\n            ➤\n          \u003c/span\u003e\n          Tap to expand\n        \u003c/button\u003e\n\n        {isContextOpen \u0026\u0026 (\n          \u003cdiv className=\"p-4 mt-2 bg-gray-100 dark:bg-gray-800 rounded-lg\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              {JSON.stringify(context, null, 2)}\n            \u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nWhen you load this in the Warpcast frames playground, you should see your own Farcaster user profile:\n\n\u003e [!WARNING]\n\u003e For the Framesgiving developer preview, context data is unauthenticated. Assume this data is spoofable and don't use it to grant privileged access to the user! Future frame SDK releases will include a mechanism fo verify context data.\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/6_context.PNG\" width=\"200\" alt=\"Context\" /\u003e\n\nThis is a lot of data, so let's hide it behind a simple toggle:\n\n```tsx\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n  const [isContextOpen, setIsContextOpen] = useState(false);\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  const toggleContext = useCallback(() =\u003e {\n    setIsContextOpen((prev) =\u003e !prev);\n  }, []);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      \u003cdiv className=\"mb-4\"\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eContext\u003c/h2\u003e\n        \u003cbutton\n          onClick={toggleContext}\n          className=\"flex items-center gap-2 transition-colors\"\n        \u003e\n          \u003cspan\n            className={`transform transition-transform ${\n              isContextOpen ? \"rotate-90\" : \"\"\n            }`}\n          \u003e\n            ➤\n          \u003c/span\u003e\n          Tap to expand\n        \u003c/button\u003e\n\n        {isContextOpen \u0026\u0026 (\n          \u003cdiv className=\"p-4 mt-2 bg-gray-100 dark:bg-gray-800 rounded-lg\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              {JSON.stringify(context, null, 2)}\n            \u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/7_toggle.png\" width=\"200\" alt=\"Toggle\" /\u003e\n\n### Invoking actions\n\nNow let's make our frame do something. We can invoke actions by calling the functions on `sdk.actions`. We've already used `sdk.actions.ready`. We can also call functions like `sdk.actions.openUrl` and `sdk.actions.close` to send commands back to the Farcaster client app.\n\nLet's start by opening an external URL. Add an `openUrl` callback that calls `sdk.actions.openUrl` and a button that calls it:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n  const [isContextOpen, setIsContextOpen] = useState(false);\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  const openUrl = useCallback(() =\u003e {\n    sdk.actions.openUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\");\n  }, []);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      {/* context toggle and data */}\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eActions\u003c/h2\u003e\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cdiv className=\"p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              sdk.actions.openUrl\n            \u003c/pre\u003e\n          \u003c/div\u003e\n          \u003cButton onClick={openUrl}\u003eOpen Link\u003c/Button\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/8_actions.png\" width=\"200\" alt=\"Actions\" /\u003e\n\nTap the button and you'll be directed to an external URL.\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/9_url.png\" width=\"200\" alt=\"URL\" /\u003e\n\nLet's add another button to call `close()`:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  const openUrl = useCallback(() =\u003e {\n    sdk.actions.openUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\");\n  }, []);\n\n  const close = useCallback(() =\u003e {\n    sdk.actions.close();\n  }, []);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eActions\u003c/h2\u003e\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cdiv className=\"p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              sdk.actions.openUrl\n            \u003c/pre\u003e\n          \u003c/div\u003e\n          \u003cButton onClick={openUrl}\u003eOpen Link\u003c/Button\u003e\n        \u003c/div\u003e\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cdiv className=\"p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              sdk.actions.close\n            \u003c/pre\u003e\n          \u003c/div\u003e\n          \u003cButton onClick={close}\u003eClose Frame\u003c/Button\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/10_close.png\" width=\"200\" alt=\"URL\" /\u003e\n\nWhen you tap this, the frame should close.\n\n### Wallet interactions\n\nFinally, let's interact with the user's connected wallet. To do so, we can use the wallet connector and Wagmi hooks we set up earlier. To start, let's read the user's connected wallet address, using `useAccount`:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\nimport { useAccount } from \"wagmi\";\n\nimport { Button } from \"~/components/ui/Button\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n\n  const { address, isConnected } = useAccount();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      {/* Context and action buttons omitted */}\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eWallet\u003c/h2\u003e\n\n        {address \u0026\u0026 (\n          \u003cdiv className=\"my-2 text-xs\"\u003e\n            Address: \u003cpre className=\"inline\"\u003e{address}\u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/10_wallet.png\" width=\"200\" alt=\"Wallet\" /\u003e\n\nIf your wallet is connected to Warpcast, you should see its address. In case it's not, let's add a connect/disconnect button. Note that we'll need to import our Wagmi config to `connect`:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\nimport { useAccount } from \"wagmi\";\n\nimport { config } from \"~/components/providers/WagmiProvider\";\nimport { Button } from \"~/components/ui/Button\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n\n  const { address, isConnected } = useAccount();\n  const { disconnect } = useDisconnect();\n  const { connect } = useConnect();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      {/* Context and action buttons omitted */}\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eWallet\u003c/h2\u003e\n\n        {address \u0026\u0026 (\n          \u003cdiv className=\"my-2 text-xs\"\u003e\n            Address: \u003cpre className=\"inline\"\u003e{address}\u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cButton\n            onClick={() =\u003e\n              isConnected\n                ? disconnect()\n                : connect({ connector: config.connectors[0] })\n            }\n          \u003e\n            {isConnected ? \"Disconnect\" : \"Connect\"}\n          \u003c/Button\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\nNow let's request a transaction. We'll use the Wagmi `useSendTransaction` hook to call the Yoink contract and `useWaitForTransactionReceipt` to watch its status.\n\n\u003e [!NOTE]\n\u003e In a more complex app, you'll probably want to use Wagmi's [useWriteContract](https://wagmi.sh/react/api/hooks/useWriteContract) hook instead. This provides better type safety and automatic encoding/decoding of calldata based on the contract ABI.\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\nimport {\n  useAccount,\n  useSendTransaction,\n  useSignMessage,\n  useSignTypedData,\n  useWaitForTransactionReceipt,\n  useDisconnect,\n  useConnect,\n} from \"wagmi\";\n\nimport { config } from \"~/components/providers/WagmiProvider\";\nimport { Button } from \"~/components/ui/Button\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n  const [txHash, setTxHash] = useState\u003cstring | null\u003e(null);\n\n  const { address, isConnected } = useAccount();\n  const {\n    sendTransaction,\n    error: sendTxError,\n    isError: isSendTxError,\n    isPending: isSendTxPending,\n  } = useSendTransaction();\n\n  const { isLoading: isConfirming, isSuccess: isConfirmed } =\n    useWaitForTransactionReceipt({\n      hash: txHash as `0x${string}`,\n    });\n\n  const { disconnect } = useDisconnect();\n  const { connect } = useConnect();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  const sendTx = useCallback(() =\u003e {\n    sendTransaction(\n      {\n        to: \"0x4bBFD120d9f352A0BEd7a014bd67913a2007a878\",\n        data: \"0x9846cd9efc000023c0\",\n      },\n      {\n        onSuccess: (hash) =\u003e {\n          setTxHash(hash);\n        },\n      }\n    );\n  }, [sendTransaction]);\n\n  const renderError = (error: Error | null) =\u003e {\n    if (!error) return null;\n    return \u003cdiv className=\"text-red-500 text-xs mt-1\"\u003e{error.message}\u003c/div\u003e;\n  };\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      {/* Context and actions omitted. */}\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eWallet\u003c/h2\u003e\n\n        {address \u0026\u0026 (\n          \u003cdiv className=\"my-2 text-xs\"\u003e\n            Address: \u003cpre className=\"inline\"\u003e{address}\u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cButton\n            onClick={() =\u003e\n              isConnected\n                ? disconnect()\n                : connect({ connector: config.connectors[0] })\n            }\n          \u003e\n            {isConnected ? \"Disconnect\" : \"Connect\"}\n          \u003c/Button\u003e\n        \u003c/div\u003e\n\n        {isConnected \u0026\u0026 (\n          \u003c\u003e\n            \u003cdiv className=\"mb-4\"\u003e\n              \u003cButton\n                onClick={sendTx}\n                disabled={!isConnected || isSendTxPending}\n                isLoading={isSendTxPending}\n              \u003e\n                Send Transaction\n              \u003c/Button\u003e\n              {isSendTxError \u0026\u0026 renderError(sendTxError)}\n              {txHash \u0026\u0026 (\n                \u003cdiv className=\"mt-2 text-xs\"\u003e\n                  \u003cdiv\u003eHash: {txHash}\u003c/div\u003e\n                  \u003cdiv\u003e\n                    Status:{\" \"}\n                    {isConfirming\n                      ? \"Confirming...\"\n                      : isConfirmed\n                      ? \"Confirmed!\"\n                      : \"Pending\"}\n                  \u003c/div\u003e\n                \u003c/div\u003e\n              )}\n            \u003c/div\u003e\n          \u003c/\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/10_tx.png\" width=\"200\" alt=\"Tx\" /\u003e\n\nTap \"Send Transaction\" and you'll be directed to your wallet.\n\n\u003cimg src=\"https://raw.githubusercontent.com/farcasterxyz/frames-v2-demo/refs/heads/main/docs/img/12_yoink.png\" width=\"200\" alt=\"Yoink\" /\u003e\n\n### Signatures\n\nFinally, let's add two new helpers for wallet signature methods. Below is the full `Demo` component:\n\n```tsx\nimport { useEffect, useCallback, useState } from \"react\";\nimport sdk, { type FrameContext } from \"@farcaster/frame-sdk\";\nimport {\n  useAccount,\n  useSendTransaction,\n  useSignMessage,\n  useSignTypedData,\n  useWaitForTransactionReceipt,\n  useDisconnect,\n  useConnect,\n} from \"wagmi\";\n\nimport { config } from \"~/components/providers/WagmiProvider\";\nimport { Button } from \"~/components/ui/Button\";\nimport { truncateAddress } from \"~/lib/truncateAddress\";\n\nexport default function Demo() {\n  const [isSDKLoaded, setIsSDKLoaded] = useState(false);\n  const [context, setContext] = useState\u003cFrameContext\u003e();\n  const [isContextOpen, setIsContextOpen] = useState(false);\n  const [txHash, setTxHash] = useState\u003cstring | null\u003e(null);\n\n  const { address, isConnected } = useAccount();\n  const {\n    sendTransaction,\n    error: sendTxError,\n    isError: isSendTxError,\n    isPending: isSendTxPending,\n  } = useSendTransaction();\n\n  const { isLoading: isConfirming, isSuccess: isConfirmed } =\n    useWaitForTransactionReceipt({\n      hash: txHash as `0x${string}`,\n    });\n\n  const {\n    signMessage,\n    error: signError,\n    isError: isSignError,\n    isPending: isSignPending,\n  } = useSignMessage();\n\n  const {\n    signTypedData,\n    error: signTypedError,\n    isError: isSignTypedError,\n    isPending: isSignTypedPending,\n  } = useSignTypedData();\n\n  const { disconnect } = useDisconnect();\n  const { connect } = useConnect();\n\n  useEffect(() =\u003e {\n    const load = async () =\u003e {\n      setContext(await sdk.context);\n      sdk.actions.ready();\n    };\n    if (sdk \u0026\u0026 !isSDKLoaded) {\n      setIsSDKLoaded(true);\n      load();\n    }\n  }, [isSDKLoaded]);\n\n  const openUrl = useCallback(() =\u003e {\n    sdk.actions.openUrl(\"https://www.youtube.com/watch?v=dQw4w9WgXcQ\");\n  }, []);\n\n  const close = useCallback(() =\u003e {\n    sdk.actions.close();\n  }, []);\n\n  const sendTx = useCallback(() =\u003e {\n    sendTransaction(\n      {\n        to: \"0x4bBFD120d9f352A0BEd7a014bd67913a2007a878\",\n        data: \"0x9846cd9efc000023c0\",\n      },\n      {\n        onSuccess: (hash) =\u003e {\n          setTxHash(hash);\n        },\n      }\n    );\n  }, [sendTransaction]);\n\n  const sign = useCallback(() =\u003e {\n    signMessage({ message: \"Hello from Frames v2!\" });\n  }, [signMessage]);\n\n  const signTyped = useCallback(() =\u003e {\n    signTypedData({\n      domain: {\n        name: \"Frames v2 Demo\",\n        version: \"1\",\n        chainId: 8453,\n      },\n      types: {\n        Message: [{ name: \"content\", type: \"string\" }],\n      },\n      message: {\n        content: \"Hello from Frames v2!\",\n      },\n      primaryType: \"Message\",\n    });\n  }, [signTypedData]);\n\n  const toggleContext = useCallback(() =\u003e {\n    setIsContextOpen((prev) =\u003e !prev);\n  }, []);\n\n  const renderError = (error: Error | null) =\u003e {\n    if (!error) return null;\n    return \u003cdiv className=\"text-red-500 text-xs mt-1\"\u003e{error.message}\u003c/div\u003e;\n  };\n\n  if (!isSDKLoaded) {\n    return \u003cdiv\u003eLoading...\u003c/div\u003e;\n  }\n\n  return (\n    \u003cdiv className=\"w-[300px] mx-auto py-4 px-2\"\u003e\n      \u003ch1 className=\"text-2xl font-bold text-center mb-4\"\u003eFrames v2 Demo\u003c/h1\u003e\n\n      \u003cdiv className=\"mb-4\"\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eContext\u003c/h2\u003e\n        \u003cbutton\n          onClick={toggleContext}\n          className=\"flex items-center gap-2 transition-colors\"\n        \u003e\n          \u003cspan\n            className={`transform transition-transform ${\n              isContextOpen ? \"rotate-90\" : \"\"\n            }`}\n          \u003e\n            ➤\n          \u003c/span\u003e\n          Tap to expand\n        \u003c/button\u003e\n\n        {isContextOpen \u0026\u0026 (\n          \u003cdiv className=\"p-4 mt-2 bg-gray-100 dark:bg-gray-800 rounded-lg\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              {JSON.stringify(context, null, 2)}\n            \u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n      \u003c/div\u003e\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eActions\u003c/h2\u003e\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cdiv className=\"p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              sdk.actions.openUrl\n            \u003c/pre\u003e\n          \u003c/div\u003e\n          \u003cButton onClick={openUrl}\u003eOpen Link\u003c/Button\u003e\n        \u003c/div\u003e\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cdiv className=\"p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2\"\u003e\n            \u003cpre className=\"font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-\"\u003e\n              sdk.actions.close\n            \u003c/pre\u003e\n          \u003c/div\u003e\n          \u003cButton onClick={close}\u003eClose Frame\u003c/Button\u003e\n        \u003c/div\u003e\n      \u003c/div\u003e\n\n      \u003cdiv\u003e\n        \u003ch2 className=\"font-2xl font-bold\"\u003eWallet\u003c/h2\u003e\n\n        {address \u0026\u0026 (\n          \u003cdiv className=\"my-2 text-xs\"\u003e\n            Address: \u003cpre className=\"inline\"\u003e{truncateAddress(address)}\u003c/pre\u003e\n          \u003c/div\u003e\n        )}\n\n        \u003cdiv className=\"mb-4\"\u003e\n          \u003cButton\n            onClick={() =\u003e\n              isConnected\n                ? disconnect()\n                : connect({ connector: config.connectors[0] })\n            }\n          \u003e\n            {isConnected ? \"Disconnect\" : \"Connect\"}\n          \u003c/Button\u003e\n        \u003c/div\u003e\n\n        {isConnected \u0026\u0026 (\n          \u003c\u003e\n            \u003cdiv className=\"mb-4\"\u003e\n              \u003cButton\n                onClick={sendTx}\n                disabled={!isConnected || isSendTxPending}\n                isLoading={isSendTxPending}\n              \u003e\n                Send Transaction\n              \u003c/Button\u003e\n              {isSendTxError \u0026\u0026 renderError(sendTxError)}\n              {txHash \u0026\u0026 (\n                \u003cdiv className=\"mt-2 text-xs\"\u003e\n                  \u003cdiv\u003eHash: {truncateAddress(txHash)}\u003c/div\u003e\n                  \u003cdiv\u003e\n                    Status:{\" \"}\n                    {isConfirming\n                      ? \"Confirming...\"\n                      : isConfirmed\n                      ? \"Confirmed!\"\n                      : \"Pending\"}\n                  \u003c/div\u003e\n                \u003c/div\u003e\n              )}\n            \u003c/div\u003e\n            \u003cdiv className=\"mb-4\"\u003e\n              \u003cButton\n                onClick={sign}\n                disabled={!isConnected || isSignPending}\n                isLoading={isSignPending}\n              \u003e\n                Sign Message\n              \u003c/Button\u003e\n              {isSignError \u0026\u0026 renderError(signError)}\n            \u003c/div\u003e\n            \u003cdiv className=\"mb-4\"\u003e\n              \u003cButton\n                onClick={signTyped}\n                disabled={!isConnected || isSignTypedPending}\n                isLoading={isSignTypedPending}\n              \u003e\n                Sign Typed Data\n              \u003c/Button\u003e\n              {isSignTypedError \u0026\u0026 renderError(signTypedError)}\n            \u003c/div\u003e\n          \u003c/\u003e\n        )}\n      \u003c/div\u003e\n    \u003c/div\u003e\n  );\n}\n```\n\nWe've build a simple v2 frame by:\n\n1. Setting up a NextJS web app\n2. Importing the Frames SDK and calling `sdk.actions.ready()`\n3. Reading the user context from `sdk.context`\n4. Invoking actions using `sdk.actions`\n5. Connecting to the user's wallet using Wagmi and `sdk.wallet.ethProvider`\n\nHappy Framesgiving! 🖼️🦃\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F10xmeme%2Fminiapp","html_url":"https://awesome.ecosyste.ms/projects/github.com%2F10xmeme%2Fminiapp","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2F10xmeme%2Fminiapp/lists"}