From 526a898f2d029fa2c410bedfedb64f37155a446a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 26 Nov 2024 09:37:50 +0100 Subject: [PATCH 01/88] chore: frames v2 test app template --- package.json | 2 +- templates/next-frames-v2-starter/.env.sample | 7 + .../next-frames-v2-starter/.eslintrc.cjs | 5 + templates/next-frames-v2-starter/.gitignore | 39 ++ .../next-frames-v2-starter/.stackblitzrc | 7 + templates/next-frames-v2-starter/README.md | 40 ++ .../app/.well-known/frames.json/route.ts | 19 + templates/next-frames-v2-starter/app/App.tsx | 242 +++++++++ .../app/WagmiProvider.tsx | 22 + .../next-frames-v2-starter/app/globals.css | 76 +++ templates/next-frames-v2-starter/app/icon.png | Bin 0 -> 407 bytes .../next-frames-v2-starter/app/layout.tsx | 55 ++ .../app/lib/connector.ts | 91 ++++ .../app/lib/truncateAddress.ts | 4 + templates/next-frames-v2-starter/app/page.tsx | 10 + .../next-frames-v2-starter/app/ui/Button.tsx | 26 + .../next-frames-v2-starter/next.config.js | 20 + templates/next-frames-v2-starter/package.json | 44 ++ .../next-frames-v2-starter/postcss.config.cjs | 6 + .../next-frames-v2-starter/public/icon.png | Bin 0 -> 2842 bytes .../next-frames-v2-starter/public/splash.png | Bin 0 -> 2842 bytes .../scripts/dev-script.js | 65 +++ .../scripts/run-stackblitz.js | 44 ++ .../next-frames-v2-starter/tailwind.config.ts | 90 +++ .../next-frames-v2-starter/tsconfig.json | 37 ++ yarn.lock | 513 ++++++++++++++++++ 26 files changed, 1463 insertions(+), 1 deletion(-) create mode 100644 templates/next-frames-v2-starter/.env.sample create mode 100644 templates/next-frames-v2-starter/.eslintrc.cjs create mode 100644 templates/next-frames-v2-starter/.gitignore create mode 100644 templates/next-frames-v2-starter/.stackblitzrc create mode 100644 templates/next-frames-v2-starter/README.md create mode 100644 templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts create mode 100644 templates/next-frames-v2-starter/app/App.tsx create mode 100644 templates/next-frames-v2-starter/app/WagmiProvider.tsx create mode 100644 templates/next-frames-v2-starter/app/globals.css create mode 100644 templates/next-frames-v2-starter/app/icon.png create mode 100644 templates/next-frames-v2-starter/app/layout.tsx create mode 100644 templates/next-frames-v2-starter/app/lib/connector.ts create mode 100644 templates/next-frames-v2-starter/app/lib/truncateAddress.ts create mode 100644 templates/next-frames-v2-starter/app/page.tsx create mode 100644 templates/next-frames-v2-starter/app/ui/Button.tsx create mode 100644 templates/next-frames-v2-starter/next.config.js create mode 100644 templates/next-frames-v2-starter/package.json create mode 100644 templates/next-frames-v2-starter/postcss.config.cjs create mode 100644 templates/next-frames-v2-starter/public/icon.png create mode 100644 templates/next-frames-v2-starter/public/splash.png create mode 100644 templates/next-frames-v2-starter/scripts/dev-script.js create mode 100644 templates/next-frames-v2-starter/scripts/run-stackblitz.js create mode 100644 templates/next-frames-v2-starter/tailwind.config.ts create mode 100644 templates/next-frames-v2-starter/tsconfig.json diff --git a/package.json b/package.json index 2ff298f1f..a14610791 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "scripts": { "build": "turbo build --filter=!./templates/* && node ./.scripts/prepare-create-frames.js", "build:ci": "turbo build --filter=!debugger --filter=!docs --filter=!template-*", - "dev": "FJS_MONOREPO=true turbo dev --filter=template-next-starter-with-examples... --filter=debugger...", + "dev": "FJS_MONOREPO=true turbo dev --filter=template-next-starter-with-examples... --filter=template-next-frames-v2-starter... --filter=debugger...", "dev:utils-starter": "FJS_MONOREPO=true turbo dev --filter=template-next-utils-starter... --filter=debugger...", "lint": "turbo lint --filter=!template-*", "test:ci": "jest --ci", diff --git a/templates/next-frames-v2-starter/.env.sample b/templates/next-frames-v2-starter/.env.sample new file mode 100644 index 000000000..1c513c816 --- /dev/null +++ b/templates/next-frames-v2-starter/.env.sample @@ -0,0 +1,7 @@ +APP_URL="http://localhost:3001" + +# Optional - Hub URL to use for the debugger. If not set, the debugger will use the default hub URL. +DEBUG_HUB_HTTP_URL= + +# Optional - debugging URL for the examples index page +NEXT_PUBLIC_DEBUGGER_URL= diff --git a/templates/next-frames-v2-starter/.eslintrc.cjs b/templates/next-frames-v2-starter/.eslintrc.cjs new file mode 100644 index 000000000..00e4aaad1 --- /dev/null +++ b/templates/next-frames-v2-starter/.eslintrc.cjs @@ -0,0 +1,5 @@ +/** @type {import("eslint").Linter.Config} */ +module.exports = { + root: true, + extends: ["next"], +}; diff --git a/templates/next-frames-v2-starter/.gitignore b/templates/next-frames-v2-starter/.gitignore new file mode 100644 index 000000000..f96c7fc29 --- /dev/null +++ b/templates/next-frames-v2-starter/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +.env +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts + +mocks.json diff --git a/templates/next-frames-v2-starter/.stackblitzrc b/templates/next-frames-v2-starter/.stackblitzrc new file mode 100644 index 000000000..4f0430644 --- /dev/null +++ b/templates/next-frames-v2-starter/.stackblitzrc @@ -0,0 +1,7 @@ +{ + "installDependencies": false, + "startCommand": "node ./scripts/run-stackblitz.js", + "env": { + "NEXT_PUBLIC_STACKBLITZ": "true" + } +} diff --git a/templates/next-frames-v2-starter/README.md b/templates/next-frames-v2-starter/README.md new file mode 100644 index 000000000..6c6e4d26d --- /dev/null +++ b/templates/next-frames-v2-starter/README.md @@ -0,0 +1,40 @@ +# Frames.js Starter Kit + +This is a boilerplate repo to get started quickly with `frames.js` + +## Quickstart + +If running from the frames.js repository itself: + +- Run `yarn` from the repository root +- Run `cd examples/framesjs-starter` + +1. Install dependencies `yarn install` + +2. Run the dev server `yarn dev` + +3. Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +4. Edit `app/page.tsx` + +5. Visit [http://localhost:3000/debug](http://localhost:3000/debug) to debug your frame. + +6. (Optional) To use a real signer (costs warps), copy `.env.sample` to `.env` and fill in the env variables following the comments provided + +## Docs, Questions and Help + +- [Frames.js Documentation](https://framesjs.org) +- [Awesome frames](https://github.com/davidfurlong/awesome-frames?tab=readme-ov-file) +- Join the [/frames-dev](https://warpcast.com/~/channel/frames-devs) channel on Farcaster to ask questions + +## If you get stuck or have feedback, [Message @df please!](https://warpcast.com/df) + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy + +```bash +vercel +``` + +more deployment links coming soon, PRs welcome! diff --git a/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts b/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts new file mode 100644 index 000000000..1e140d1a4 --- /dev/null +++ b/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts @@ -0,0 +1,19 @@ +export async function GET() { + const appUrl = process.env.APP_URL; + + const config = { + config: { + version: "0.0.0", + name: "Frames v2 Demo", + icon: new URL("/icon.png", appUrl).toString(), + splashImage: new URL("/splash.png", appUrl).toString(), + splashBackgroundColor: "#f7f7f7", + homeUrl: appUrl, + fid: 0, + key: "", + signature: "", + }, + }; + + return Response.json(config); +} diff --git a/templates/next-frames-v2-starter/app/App.tsx b/templates/next-frames-v2-starter/app/App.tsx new file mode 100644 index 000000000..c5b5e65cf --- /dev/null +++ b/templates/next-frames-v2-starter/app/App.tsx @@ -0,0 +1,242 @@ +import { useEffect, useCallback, useState } from "react"; +import sdk, { type FrameContext } from "@farcaster/frame-sdk"; +import { + useAccount, + useSendTransaction, + useSignMessage, + useSignTypedData, + useWaitForTransactionReceipt, + useDisconnect, + useConnect, +} from "wagmi"; + +import { config } from "./WagmiProvider"; +import { Button } from "./ui/Button"; +import { truncateAddress } from "./lib/truncateAddress"; + +export default function Demo() { + const [isSDKLoaded, setIsSDKLoaded] = useState(false); + const [context, setContext] = useState(); + const [isContextOpen, setIsContextOpen] = useState(false); + const [txHash, setTxHash] = useState(null); + + const { address, isConnected } = useAccount(); + const { + sendTransaction, + error: sendTxError, + isError: isSendTxError, + isPending: isSendTxPending, + } = useSendTransaction(); + + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ + hash: txHash as `0x${string}`, + }); + + const { + signMessage, + error: signError, + isError: isSignError, + isPending: isSignPending, + } = useSignMessage(); + + const { + signTypedData, + error: signTypedError, + isError: isSignTypedError, + isPending: isSignTypedPending, + } = useSignTypedData(); + + const { disconnect } = useDisconnect(); + const { connect } = useConnect(); + + useEffect(() => { + const load = async () => { + setContext(await sdk.context); + sdk.actions.ready(); + }; + if (sdk && !isSDKLoaded) { + setIsSDKLoaded(true); + load(); + } + }, [isSDKLoaded]); + + const openUrl = useCallback(() => { + sdk.actions.openUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); + }, []); + + const close = useCallback(() => { + sdk.actions.close(); + }, []); + + const sendTx = useCallback(() => { + sendTransaction( + { + to: "0x4bBFD120d9f352A0BEd7a014bd67913a2007a878", + data: "0x9846cd9efc000023c0", + }, + { + onSuccess: (hash) => { + setTxHash(hash); + }, + } + ); + }, [sendTransaction]); + + const sign = useCallback(() => { + signMessage({ message: "Hello from Frames v2!" }); + }, [signMessage]); + + const signTyped = useCallback(() => { + signTypedData({ + domain: { + name: "Frames v2 Demo", + version: "1", + chainId: 8453, + }, + types: { + Message: [{ name: "content", type: "string" }], + }, + message: { + content: "Hello from Frames v2!", + }, + primaryType: "Message", + }); + }, [signTypedData]); + + const toggleContext = useCallback(() => { + setIsContextOpen((prev) => !prev); + }, []); + + const renderError = (error: Error | null) => { + if (!error) return null; + return
{error.message}
; + }; + + if (!isSDKLoaded) { + return
Loading...
; + } + + return ( +
+

Frames v2 Demo

+ +
+

Context

+ + + {isContextOpen && ( +
+
+              {JSON.stringify(context, null, 2)}
+            
+
+ )} +
+ +
+

Actions

+ +
+
+
+              sdk.actions.openUrl
+            
+
+ +
+ +
+
+
+              sdk.actions.close
+            
+
+ +
+
+ +
+

Wallet

+ + {address && ( +
+ Address:
{truncateAddress(address)}
+
+ )} + +
+ +
+ + {isConnected && ( + <> +
+ + {isSendTxError && renderError(sendTxError)} + {txHash && ( +
+
Hash: {truncateAddress(txHash)}
+
+ Status:{" "} + {isConfirming + ? "Confirming..." + : isConfirmed + ? "Confirmed!" + : "Pending"} +
+
+ )} +
+
+ + {isSignError && renderError(signError)} +
+
+ + {isSignTypedError && renderError(signTypedError)} +
+ + )} +
+
+ ); +} diff --git a/templates/next-frames-v2-starter/app/WagmiProvider.tsx b/templates/next-frames-v2-starter/app/WagmiProvider.tsx new file mode 100644 index 000000000..8f08c8887 --- /dev/null +++ b/templates/next-frames-v2-starter/app/WagmiProvider.tsx @@ -0,0 +1,22 @@ +import { createConfig, http, WagmiProvider } from "wagmi"; +import { base } from "wagmi/chains"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { frameConnector } from "./lib/connector"; + +export const config = createConfig({ + chains: [base], + transports: { + [base.id]: http(), + }, + connectors: [frameConnector()], +}); + +const queryClient = new QueryClient(); + +export function WagmiConfig({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/templates/next-frames-v2-starter/app/globals.css b/templates/next-frames-v2-starter/app/globals.css new file mode 100644 index 000000000..6a7572500 --- /dev/null +++ b/templates/next-frames-v2-starter/app/globals.css @@ -0,0 +1,76 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 222.2 84% 4.9%; + + --card: 0 0% 100%; + --card-foreground: 222.2 84% 4.9%; + + --popover: 0 0% 100%; + --popover-foreground: 222.2 84% 4.9%; + + --primary: 222.2 47.4% 11.2%; + --primary-foreground: 210 40% 98%; + + --secondary: 210 40% 96.1%; + --secondary-foreground: 222.2 47.4% 11.2%; + + --muted: 210 40% 96.1%; + --muted-foreground: 215.4 16.3% 46.9%; + + --accent: 210 40% 96.1%; + --accent-foreground: 222.2 47.4% 11.2%; + + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 210 40% 98%; + + --border: 214.3 31.8% 91.4%; + --input: 214.3 31.8% 91.4%; + --ring: 222.2 84% 4.9%; + + --radius: 0.5rem; + } + + .dark { + --background: 222.2 84% 4.9%; + --foreground: 210 40% 98%; + + --card: 222.2 84% 4.9%; + --card-foreground: 210 40% 98%; + + --popover: 222.2 84% 4.9%; + --popover-foreground: 210 40% 98%; + + --primary: 210 40% 98%; + --primary-foreground: 222.2 47.4% 11.2%; + + --secondary: 217.2 32.6% 17.5%; + --secondary-foreground: 210 40% 98%; + + --muted: 217.2 32.6% 17.5%; + --muted-foreground: 215 20.2% 65.1%; + + --accent: 217.2 32.6% 17.5%; + --accent-foreground: 210 40% 98%; + + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 210 40% 98%; + + --border: 217.2 32.6% 17.5%; + --input: 217.2 32.6% 17.5%; + --ring: 212.7 26.8% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} \ No newline at end of file diff --git a/templates/next-frames-v2-starter/app/icon.png b/templates/next-frames-v2-starter/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..01fc9b6e4c61feaf3162adfd69c9b1b85b25b24a GIT binary patch literal 407 zcmV;I0cie-P)awL067420QUeK!0k8o!q%(3dJDl?u-9G>;2dB)03HB(<&iKkaU3;4pqQ`W zLEa1h%$v#oI+q+Y$QK&M228UaRt5zrnt zyd{n!>~>pJNt&j3SpX$JDyW)vwt)PaF{Uj%9QJaT$UO&xZ*9I10w;y#D+Rc&`_v7+ z+*&z57BN}(e74Z$lN+i8w6g_t132f}!rRqQ?D=DDJ`E%<3!us6o#B+uLfMXYku6{j zLtXTGmW<_8Oe>D!bv`g=_%z + {children} + + ); +} diff --git a/templates/next-frames-v2-starter/app/lib/connector.ts b/templates/next-frames-v2-starter/app/lib/connector.ts new file mode 100644 index 000000000..30446f551 --- /dev/null +++ b/templates/next-frames-v2-starter/app/lib/connector.ts @@ -0,0 +1,91 @@ +import sdk from "@farcaster/frame-sdk"; +import { SwitchChainError, fromHex, getAddress, numberToHex } from "viem"; +import { ChainNotConfiguredError, createConnector } from "wagmi"; + +frameConnector.type = "frameConnector" as const; + +export function frameConnector() { + let connected = true; + + return createConnector((config) => ({ + id: "farcaster", + name: "Farcaster Wallet", + type: frameConnector.type, + + async setup() { + this.connect({ chainId: config.chains[0].id }); + }, + async connect({ chainId } = {}) { + const provider = await this.getProvider(); + const accounts = await provider.request({ + method: "eth_requestAccounts", + }); + + let currentChainId = await this.getChainId(); + if (chainId && currentChainId !== chainId) { + const chain = await this.switchChain!({ chainId }); + currentChainId = chain.id; + } + + connected = true; + + return { + accounts: accounts.map((x) => getAddress(x)), + chainId: currentChainId, + }; + }, + async disconnect() { + connected = false; + }, + async getAccounts() { + if (!connected) throw new Error("Not connected"); + const provider = await this.getProvider(); + const accounts = await provider.request({ + method: "eth_requestAccounts", + }); + return accounts.map((x) => getAddress(x)); + }, + async getChainId() { + const provider = await this.getProvider(); + const hexChainId = await provider.request({ method: "eth_chainId" }); + return fromHex(hexChainId, "number"); + }, + async isAuthorized() { + if (!connected) { + return false; + } + + const accounts = await this.getAccounts(); + return !!accounts.length; + }, + async switchChain({ chainId }) { + const provider = await this.getProvider(); + const chain = config.chains.find((x) => x.id === chainId); + if (!chain) throw new SwitchChainError(new ChainNotConfiguredError()); + + await provider.request({ + method: "wallet_switchEthereumChain", + params: [{ chainId: numberToHex(chainId) }], + }); + return chain; + }, + onAccountsChanged(accounts) { + if (accounts.length === 0) this.onDisconnect(); + else + config.emitter.emit("change", { + accounts: accounts.map((x) => getAddress(x)), + }); + }, + onChainChanged(chain) { + const chainId = Number(chain); + config.emitter.emit("change", { chainId }); + }, + async onDisconnect() { + config.emitter.emit("disconnect"); + connected = false; + }, + async getProvider() { + return sdk.wallet.ethProvider; + }, + })); +} diff --git a/templates/next-frames-v2-starter/app/lib/truncateAddress.ts b/templates/next-frames-v2-starter/app/lib/truncateAddress.ts new file mode 100644 index 000000000..f14ecbc87 --- /dev/null +++ b/templates/next-frames-v2-starter/app/lib/truncateAddress.ts @@ -0,0 +1,4 @@ +export function truncateAddress(address: string) { + if (!address) return ""; + return `${address.slice(0, 14)}...${address.slice(-12)}`; +} diff --git a/templates/next-frames-v2-starter/app/page.tsx b/templates/next-frames-v2-starter/app/page.tsx new file mode 100644 index 000000000..4b2758654 --- /dev/null +++ b/templates/next-frames-v2-starter/app/page.tsx @@ -0,0 +1,10 @@ +import { WagmiConfig } from "./WagmiConfig"; +import { App } from "./app"; + +export default async function Home() { + return ( + + + + ); +} diff --git a/templates/next-frames-v2-starter/app/ui/Button.tsx b/templates/next-frames-v2-starter/app/ui/Button.tsx new file mode 100644 index 000000000..e5a8fc5b0 --- /dev/null +++ b/templates/next-frames-v2-starter/app/ui/Button.tsx @@ -0,0 +1,26 @@ +interface ButtonProps extends React.ButtonHTMLAttributes { + children: React.ReactNode; + isLoading?: boolean; +} + +export function Button({ + children, + className = "", + isLoading = false, + ...props +}: ButtonProps) { + return ( + + ); +} diff --git a/templates/next-frames-v2-starter/next.config.js b/templates/next-frames-v2-starter/next.config.js new file mode 100644 index 000000000..ff425b0e1 --- /dev/null +++ b/templates/next-frames-v2-starter/next.config.js @@ -0,0 +1,20 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + // prevent double render on dev mode, which causes 2 frames to exist + reactStrictMode: false, + images: { + minimumCacheTTL: 1, // to allow dynamic images in case you are previewing them using next/image + remotePatterns: [ + { + hostname: "*", + protocol: "http", + }, + { + hostname: "*", + protocol: "https", + }, + ], + }, +}; + +export default nextConfig; diff --git a/templates/next-frames-v2-starter/package.json b/templates/next-frames-v2-starter/package.json new file mode 100644 index 000000000..c6115d494 --- /dev/null +++ b/templates/next-frames-v2-starter/package.json @@ -0,0 +1,44 @@ +{ + "name": "template-next-frames-v2-starter", + "version": "0.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "node ./scripts/dev-script.js", + "dev:monorepo": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@farcaster/frame-sdk": "^0.0.2", + "@tanstack/react-query": "^5.61.3", + "clsx": "^2.1.0", + "frames.js": "^0.20.0", + "next": "^14.1.4", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "tailwindcss-animate": "^1.0.7", + "viem": "^2.21.50", + "wagmi": "^2.13.0" + }, + "engines": { + "node": ">=18.17.0" + }, + "devDependencies": { + "@frames.js/debugger": "^0.3.19", + "@types/node": "^18.17.0", + "@types/react": "^18.2.0", + "@types/react-dom": "^18.2.0", + "@types/uuid": "^10.0.0", + "autoprefixer": "^10.0.1", + "concurrently": "^8.2.2", + "dotenv": "^16.4.5", + "eslint": "^8.56.0", + "eslint-config-next": "^14.1.0", + "is-port-reachable": "^4.0.0", + "postcss": "^8", + "tailwindcss": "^3.3.0", + "typescript": "^5.4.5" + } +} diff --git a/templates/next-frames-v2-starter/postcss.config.cjs b/templates/next-frames-v2-starter/postcss.config.cjs new file mode 100644 index 000000000..12a703d90 --- /dev/null +++ b/templates/next-frames-v2-starter/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/templates/next-frames-v2-starter/public/icon.png b/templates/next-frames-v2-starter/public/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..8031ae5130bb461eb2a3956b7b98b5719264ef67 GIT binary patch literal 2842 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 BHB} + */ +async function getExamplesFromDirectory(port) { + return [ + { + title: "Frames v2 Starter", + url: `http://localhost:${port}`, + }, + ]; +} + +const nextPort = await getOpenPort(3001); +const debuggerPort = await getOpenPort(3010); +let command = "npm"; +let args = ["run", "dev:monorepo"]; + +// this sets hub url for debugger +process.env.DEBUGGER_HUB_HTTP_URL = `http://localhost:${debuggerPort}/hub`; +// this sets the app url for the starter so the initial server side render works properly +process.env.APP_URL = `http://localhost:${nextPort}`; + +if (!process.env.FJS_MONOREPO) { + const url = `http://localhost:${nextPort}`; + + const examples = await getExamplesFromDirectory(nextPort); + + process.env.DEBUGGER_EXAMPLES_JSON = JSON.stringify(examples); + + command = "concurrently"; + args = [ + "--kill-others", + `"next dev -p ${nextPort}"`, + `"frames --port ${debuggerPort} --url ${url} ${process.env.FARCASTER_DEVELOPER_FID ? `--fid '${process.env.FARCASTER_DEVELOPER_FID}'` : ""} ${process.env.FARCASTER_DEVELOPER_MNEMONIC ? `--fdm '${process.env.FARCASTER_DEVELOPER_MNEMONIC}'` : ""} "`, + ]; +} + +// Spawn the child process +const child = spawn(command, args, { + stdio: "inherit", + shell: true, + env: { + ...process.env, + PORT: nextPort, + }, +}); + +child.on("error", (error) => { + console.error(`spawn error: ${error}`); +}); diff --git a/templates/next-frames-v2-starter/scripts/run-stackblitz.js b/templates/next-frames-v2-starter/scripts/run-stackblitz.js new file mode 100644 index 000000000..6ecda507f --- /dev/null +++ b/templates/next-frames-v2-starter/scripts/run-stackblitz.js @@ -0,0 +1,44 @@ +import fs from "node:fs"; +import { spawnSync, spawn } from "node:child_process"; + +console.log("Pinning next.js version to 14.1.4"); +const packageJsonPath = "package.json"; +const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8")); + +packageJson.dependencies.next = "14.1.4"; +packageJson.devDependencies["@next/swc-wasm-nodejs"] = "14.1.4"; + +fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); + +console.log("Installing dependencies"); +spawnSync("yarn", ["install"], { + shell: true, + stdio: "inherit", +}); + +console.log("Running debugger and examples server"); + +import("dotenv/config").then(() => { + // first start debugger server (it opens the preview on stackblitz automatically) + const debuggerServer = spawn("yarn frames", { + shell: true, + stdio: "inherit", + }); + + debuggerServer.on("error", (error) => { + console.error("debugger spawn error", error); + }); + + debuggerServer.on("spawn", async () => { + await new Promise((resolve) => setTimeout(resolve, 2000)); + // now open examples server + const server = spawn("yarn dev:monorepo", { + shell: true, + stdio: "inherit", + }); + + server.on("error", (error) => { + console.error("server spawn error", error); + }); + }); +}); diff --git a/templates/next-frames-v2-starter/tailwind.config.ts b/templates/next-frames-v2-starter/tailwind.config.ts new file mode 100644 index 000000000..d82220667 --- /dev/null +++ b/templates/next-frames-v2-starter/tailwind.config.ts @@ -0,0 +1,90 @@ +import type { Config } from "tailwindcss"; + +const config = { + darkMode: ["class"], + content: [ + "./pages/**/*.{ts,tsx}", + "./components/**/*.{ts,tsx}", + "./app/**/*.{ts,tsx}", + "./@/**/*.{ts,tsx}", + "./src/**/*.{ts,tsx}", + // js files primarily because in dist + "./node_modules/frames.js/dist/**/*.{ts,tsx,js,css}", + "./node_modules/@frames.js/render/dist/*.{ts,tsx,js,css}", + "./node_modules/@frames.js/render/dist/**/*.{ts,tsx,js,css}", + + // monorepo weirdness + "../../node_modules/frames.js/dist/**/*.{ts,tsx,js,css}", + "../../node_modules/@frames.js/render/dist/*.{ts,tsx,js,css}", + "../../node_modules/@frames.js/render/dist/**/*.{ts,tsx,js,css}", + ], + prefix: "", + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: "0" }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: "0" }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; + +export default config; diff --git a/templates/next-frames-v2-starter/tsconfig.json b/templates/next-frames-v2-starter/tsconfig.json new file mode 100644 index 000000000..d3b36f3c1 --- /dev/null +++ b/templates/next-frames-v2-starter/tsconfig.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "display": "Default", + "compilerOptions": { + "esModuleInterop": true, + "incremental": false, + "isolatedModules": true, + "lib": ["es2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "Bundler", + "noEmit": true, + "noUncheckedIndexedAccess": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./@/*"] + }, + "jsx": "preserve", + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true, + "target": "ES2022", + "allowJs": true + }, + "include": [ + "next-env.d.ts", + "next.config.js", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts" + ], + "exclude": ["node_modules"] +} diff --git a/yarn.lock b/yarn.lock index 1a632e840..7c6774d87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,6 +12,11 @@ resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.10.0.tgz#d2a39395c587e092d77cbbc80acf956a54f38bf7" integrity sha512-nA9XHtlAkYfJxY7bce8DcN7eKxWWCWkU+1GR9d+U6MbNpfwQp8TI7vqOsBsMcHoT4mBu2kypKoSKnghEzOOq5Q== +"@adraffy/ens-normalize@^1.10.1": + version "1.11.0" + resolved "https://registry.yarnpkg.com/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz#42cc67c5baa407ac25059fcd7d405cc5ecdb0c33" + integrity sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg== + "@alloc/quick-lru@^5.2.0": version "5.2.0" resolved "https://registry.yarnpkg.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz#7bf68b20c0a350f936915fcae06f58e32007ce30" @@ -1393,6 +1398,16 @@ preact "^10.16.0" sha.js "^2.4.11" +"@coinbase/wallet-sdk@4.2.3": + version "4.2.3" + resolved "https://registry.yarnpkg.com/@coinbase/wallet-sdk/-/wallet-sdk-4.2.3.tgz#a30fa0605b24bc42c37f52a62d2442bcbb7734af" + integrity sha512-BcyHZ/Ec84z0emORzqdXDv4P0oV+tV3a0OirfA8Ko1JGBIAVvB+hzLvZzCDvnuZx7MTK+Dd8Y9Tjlo446BpCIg== + dependencies: + "@noble/hashes" "^1.4.0" + clsx "^1.2.1" + eventemitter3 "^5.0.1" + preact "^10.24.2" + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.yarnpkg.com/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -1405,6 +1420,11 @@ dependencies: "@jridgewell/trace-mapping" "0.3.9" +"@ecies/ciphers@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@ecies/ciphers/-/ciphers-0.2.1.tgz#a3119516fb55d27ed2d21c497b1c4988f0b4ca02" + integrity sha512-ezMihhjW24VNK/2qQR7lH8xCQY24nk0XHF/kwJ1OuiiY5iEwQXOcKVSy47fSoHPRG8gVGXcK5SgtONDk5xMwtQ== + "@emotion/cache@^10.0.27": version "10.0.29" resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-10.0.29.tgz#87e7e64f412c060102d589fe7c6dc042e6f9d1e0" @@ -2392,6 +2412,22 @@ neverthrow "^6.0.0" viem "^1.12.2" +"@farcaster/frame-core@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.4.tgz#651398056526abd361d342aa5c22f28403ec791f" + integrity sha512-5g2aGjpS2D2amNppRD01nWlxvV0PwN0zV8fJuwJT0vcD0c72W6c8zWOMidXAhzJ/BXf9hNekLxxMpWrZoqDNsw== + dependencies: + ox "^0.1.6" + +"@farcaster/frame-sdk@^0.0.2": + version "0.0.2" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.2.tgz#a052898d058cb3410bdbf02491727cca59dc6449" + integrity sha512-d+7w662rfBvbTFpaRHtP8U3zjc7GNVEqsu1/2jrci/JGwWCSFwFUmH0V/kINzsYBcrn1hT57c5is8cshrYy83A== + dependencies: + "@farcaster/frame-core" "^0.0.4" + comlink "^4.4.2" + eventemitter3 "^5.0.1" + "@fastify/busboy@^2.0.0": version "2.1.1" resolved "https://registry.yarnpkg.com/@fastify/busboy/-/busboy-2.1.1.tgz#b9da6a878a371829a0502c9b6c1c143ef6663f4d" @@ -3137,6 +3173,15 @@ "@metamask/safe-event-emitter" "^3.0.0" "@metamask/utils" "^8.3.0" +"@metamask/json-rpc-engine@^8.0.1", "@metamask/json-rpc-engine@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-engine/-/json-rpc-engine-8.0.2.tgz#29510a871a8edef892f838ee854db18de0bf0d14" + integrity sha512-IoQPmql8q7ABLruW7i4EYVHWUbF74yrp63bRuXV5Zf9BQwcn5H9Ww1eLtROYvI1bUXwOiHZ6qT5CWTrDc/t/AA== + dependencies: + "@metamask/rpc-errors" "^6.2.1" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + "@metamask/json-rpc-middleware-stream@^6.0.2": version "6.0.2" resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-6.0.2.tgz#75852ce481f8f9f091edbfc04ffdf964f8f3cabd" @@ -3147,6 +3192,16 @@ "@metamask/utils" "^8.3.0" readable-stream "^3.6.2" +"@metamask/json-rpc-middleware-stream@^7.0.1": + version "7.0.2" + resolved "https://registry.yarnpkg.com/@metamask/json-rpc-middleware-stream/-/json-rpc-middleware-stream-7.0.2.tgz#2e8b2cbc38968e3c6239a9144c35bbb08a8fb57d" + integrity sha512-yUdzsJK04Ev98Ck4D7lmRNQ8FPioXYhEUZOMS01LXW8qTvPGiRVXmVltj2p4wrLkh0vW7u6nv0mNl5xzC5Qmfg== + dependencies: + "@metamask/json-rpc-engine" "^8.0.2" + "@metamask/safe-event-emitter" "^3.0.0" + "@metamask/utils" "^8.3.0" + readable-stream "^3.6.2" + "@metamask/object-multiplex@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@metamask/object-multiplex/-/object-multiplex-2.0.0.tgz#aa6e4aa7b4e2f457ea4bb51cd7281d931e0aa35d" @@ -3162,6 +3217,24 @@ dependencies: bowser "^2.9.0" +"@metamask/providers@16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-16.1.0.tgz#7da593d17c541580fa3beab8d9d8a9b9ce19ea07" + integrity sha512-znVCvux30+3SaUwcUGaSf+pUckzT5ukPRpcBmy+muBLC0yaWnBcvDqGfcsw6CBIenUdFrVoAFa8B6jsuCY/a+g== + dependencies: + "@metamask/json-rpc-engine" "^8.0.1" + "@metamask/json-rpc-middleware-stream" "^7.0.1" + "@metamask/object-multiplex" "^2.0.0" + "@metamask/rpc-errors" "^6.2.1" + "@metamask/safe-event-emitter" "^3.1.1" + "@metamask/utils" "^8.3.0" + detect-browser "^5.2.0" + extension-port-stream "^3.0.0" + fast-deep-equal "^3.1.3" + is-stream "^2.0.0" + readable-stream "^3.6.2" + webextension-polyfill "^0.10.0" + "@metamask/providers@^15.0.0": version "15.0.0" resolved "https://registry.yarnpkg.com/@metamask/providers/-/providers-15.0.0.tgz#e8957bb89d2f3379b32b60117d79a141e44db2bc" @@ -3198,6 +3271,11 @@ resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.1.tgz#e89b840a7af8097a8ed4953d8dc8470d1302d3ef" integrity sha512-ihb3B0T/wJm1eUuArYP4lCTSEoZsClHhuWyfo/kMX3m/odpqNcPfsz5O2A3NT7dXCAgWPGDQGPqygCpgeniKMw== +"@metamask/safe-event-emitter@^3.1.1": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@metamask/safe-event-emitter/-/safe-event-emitter-3.1.2.tgz#bfac8c7a1a149b5bbfe98f59fbfea512dfa3bad4" + integrity sha512-5yb2gMI1BDm0JybZezeoX/3XhPDOtTbcFvpTXM9kxsoZjPZFh4XciqRbpD6N86HYZqWDhEaKUDuOyR0sQHEjMA== + "@metamask/sdk-communication-layer@0.20.5": version "0.20.5" resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.20.5.tgz#b472fe223319a25a060155ea904f7c66062102b2" @@ -3209,6 +3287,17 @@ utf-8-validate "^6.0.3" uuid "^8.3.2" +"@metamask/sdk-communication-layer@0.30.0": + version "0.30.0" + resolved "https://registry.yarnpkg.com/@metamask/sdk-communication-layer/-/sdk-communication-layer-0.30.0.tgz#2bd252cfce3ac4260a6c8c9359732ab5e839b75e" + integrity sha512-q5nbdYkAf76MsZxi1l5MJEAyd8sY9jLRapC8a7x1Q1BNV4rzQeFeux/d0mJ/jTR2LAwbnLZs2rL226AM75oK4w== + dependencies: + bufferutil "^4.0.8" + date-fns "^2.29.3" + debug "^4.3.4" + utf-8-validate "^5.0.2" + uuid "^8.3.2" + "@metamask/sdk-install-modal-web@0.20.4": version "0.20.4" resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.20.4.tgz#50b97be4e3be17c3622281c2ad9fc49769e33216" @@ -3216,6 +3305,13 @@ dependencies: qr-code-styling "^1.6.0-rc.1" +"@metamask/sdk-install-modal-web@0.30.0": + version "0.30.0" + resolved "https://registry.yarnpkg.com/@metamask/sdk-install-modal-web/-/sdk-install-modal-web-0.30.0.tgz#9ec634201b1b47bb30064f42ae0efba7f204bb0a" + integrity sha512-1gT533Huja9tK3cmttvcpZirRAtWJ7vnYH+lnNRKEj2xIP335Df2cOwS+zqNC4GlRCZw7A3IsTjIzlKoxBY1uQ== + dependencies: + qr-code-styling "^1.6.0-rc.1" + "@metamask/sdk@0.20.5": version "0.20.5" resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.20.5.tgz#ae90b4e5108f2a0e5f5536e66354c3e31b121af9" @@ -3244,6 +3340,32 @@ util "^0.12.4" uuid "^8.3.2" +"@metamask/sdk@0.30.1": + version "0.30.1" + resolved "https://registry.yarnpkg.com/@metamask/sdk/-/sdk-0.30.1.tgz#63126ad769566098000cc3c2cd513d18808471f3" + integrity sha512-NelEjJZsF5wVpSQELpmvXtnS9+C6HdxGQ4GB9jMRzeejphmPyKqmrIGM6XtaPrJtlpX+40AcJ2dtBQcjJVzpbQ== + dependencies: + "@metamask/onboarding" "^1.0.1" + "@metamask/providers" "16.1.0" + "@metamask/sdk-communication-layer" "0.30.0" + "@metamask/sdk-install-modal-web" "0.30.0" + bowser "^2.9.0" + cross-fetch "^4.0.0" + debug "^4.3.4" + eciesjs "^0.4.8" + eth-rpc-errors "^4.0.3" + eventemitter2 "^6.4.7" + i18next "23.11.5" + i18next-browser-languagedetector "7.1.0" + obj-multiplex "^1.0.0" + pump "^3.0.0" + qrcode-terminal-nooctal "^0.12.1" + react-native-webview "^11.26.0" + readable-stream "^3.6.2" + socket.io-client "^4.5.1" + util "^0.12.4" + uuid "^8.3.2" + "@metamask/utils@^5.0.1": version "5.0.2" resolved "https://registry.yarnpkg.com/@metamask/utils/-/utils-5.0.2.tgz#140ba5061d90d9dac0280c19cab101bc18c8857c" @@ -3468,6 +3590,11 @@ dependencies: eslint-scope "5.1.1" +"@noble/ciphers@^1.0.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@noble/ciphers/-/ciphers-1.1.0.tgz#ebe2d3aa656c1afe6a14bb95af73f3851c162d73" + integrity sha512-gwcX7IKSuCtlepJVa6sDLMB2EDaoLguFL6HxagKeFIzWGRfFE3mwcHs8mjx4yQY+rV736XGBhfl6Lw80YrTDTw== + "@noble/curves@1.2.0", "@noble/curves@~1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.2.0.tgz#92d7e12e4e49b23105a2555c6984d41733d65c35" @@ -3489,6 +3616,20 @@ dependencies: "@noble/hashes" "1.4.0" +"@noble/curves@1.6.0", "@noble/curves@~1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.6.0.tgz#be5296ebcd5a1730fccea4786d420f87abfeb40b" + integrity sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ== + dependencies: + "@noble/hashes" "1.5.0" + +"@noble/curves@^1.4.0", "@noble/curves@^1.6.0", "@noble/curves@~1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.7.0.tgz#0512360622439256df892f21d25b388f52505e45" + integrity sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== + dependencies: + "@noble/hashes" "1.6.0" + "@noble/curves@~1.4.0": version "1.4.2" resolved "https://registry.yarnpkg.com/@noble/curves/-/curves-1.4.2.tgz#40309198c76ed71bc6dbf7ba24e81ceb4d0d1fe9" @@ -3516,6 +3657,21 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.4.0.tgz#45814aa329f30e4fe0ba49426f49dfccdd066426" integrity sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg== +"@noble/hashes@1.5.0", "@noble/hashes@~1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" + integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== + +"@noble/hashes@1.6.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.0.tgz#d4bfb516ad6e7b5111c216a5cc7075f4cf19e6c5" + integrity sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== + +"@noble/hashes@^1.5.0", "@noble/hashes@~1.6.0": + version "1.6.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.6.1.tgz#df6e5943edcea504bac61395926d6fd67869a0d5" + integrity sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== + "@noble/secp256k1@1.7.1": version "1.7.1" resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c" @@ -4957,6 +5113,14 @@ "@safe-global/safe-apps-sdk" "^8.1.0" events "^3.3.0" +"@safe-global/safe-apps-provider@0.18.4": + version "0.18.4" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-provider/-/safe-apps-provider-0.18.4.tgz#53df912aa20d933f6b14c5bcb0737a8cd47def57" + integrity sha512-SWYeG3gyTO6wGHMSokfHakZ9isByn2mHsM0VohIorYFFEyGGmJ89btnTm+DqDUSoQtvWAatZB7XNy6CaYMvqtg== + dependencies: + "@safe-global/safe-apps-sdk" "^9.1.0" + events "^3.3.0" + "@safe-global/safe-apps-sdk@8.1.0", "@safe-global/safe-apps-sdk@^8.1.0": version "8.1.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-8.1.0.tgz#d1d0c69cd2bf4eef8a79c5d677d16971926aa64a" @@ -4965,6 +5129,14 @@ "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" viem "^1.0.0" +"@safe-global/safe-apps-sdk@9.1.0", "@safe-global/safe-apps-sdk@^9.1.0": + version "9.1.0" + resolved "https://registry.yarnpkg.com/@safe-global/safe-apps-sdk/-/safe-apps-sdk-9.1.0.tgz#0e65913e0f202e529ed3c846e0f5a98c2d35aa98" + integrity sha512-N5p/ulfnnA2Pi2M3YeWjULeWbjo7ei22JwU/IXnhoHzKq3pYCN6ynL9mJBOlvDVv892EgLPCWCOwQk/uBT2v0Q== + dependencies: + "@safe-global/safe-gateway-typescript-sdk" "^3.5.3" + viem "^2.1.1" + "@safe-global/safe-gateway-typescript-sdk@^3.5.3": version "3.19.0" resolved "https://registry.yarnpkg.com/@safe-global/safe-gateway-typescript-sdk/-/safe-gateway-typescript-sdk-3.19.0.tgz#18637c205c83bfc0a6be5fddbf202d6bb4927302" @@ -4980,6 +5152,16 @@ resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.7.tgz#fe973311a5c6267846aa131bc72e96c5d40d2b30" integrity sha512-PPNYBslrLNNUQ/Yad37MHYsNQtK67EhWb6WtSvNLLPo7SdVZgkUjD6Dg+5On7zNwmskf8OX7I7Nx5oN+MIWE0g== +"@scure/base@~1.1.7", "@scure/base@~1.1.8": + version "1.1.9" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.9.tgz#e5e142fbbfe251091f9c5f1dd4c834ac04c3dbd1" + integrity sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg== + +"@scure/base@~1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.2.1.tgz#dd0b2a533063ca612c17aa9ad26424a2ff5aa865" + integrity sha512-DGmGtC8Tt63J5GfHgfl5CuAXh96VF/LD8K9Hr/Gv0J2lAoRGlPOMpqMpMbCTOoOJMZCk2Xt+DskdDyn6dEFdzQ== + "@scure/bip32@1.3.2": version "1.3.2" resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.3.2.tgz#90e78c027d5e30f0b22c1f8d50ff12f3fb7559f8" @@ -5007,6 +5189,24 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip32@1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.5.0.tgz#dd4a2e1b8a9da60e012e776d954c4186db6328e6" + integrity sha512-8EnFYkqEQdnkuGBVpCzKxyIwDCBLDVj3oiX0EKUFre/tOjL/Hqba1D6n/8RcmaQy4f95qQFrO2A8Sr6ybh4NRw== + dependencies: + "@noble/curves" "~1.6.0" + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.7" + +"@scure/bip32@^1.5.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@scure/bip32/-/bip32-1.6.0.tgz#6dbc6b4af7c9101b351f41231a879d8da47e0891" + integrity sha512-82q1QfklrUUdXJzjuRU7iG7D7XiFx5PHYVS0+oeNKhyDLT7WPqs6pBcM2W5ZdwOwKCwoE1Vy1se+DHjcXwCYnA== + dependencies: + "@noble/curves" "~1.7.0" + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + "@scure/bip39@1.2.1": version "1.2.1" resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.2.1.tgz#5cee8978656b272a917b7871c981e0541ad6ac2a" @@ -5031,6 +5231,22 @@ "@noble/hashes" "~1.4.0" "@scure/base" "~1.1.6" +"@scure/bip39@1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.4.0.tgz#664d4f851564e2e1d4bffa0339f9546ea55960a6" + integrity sha512-BEEm6p8IueV/ZTfQLp/0vhw4NPnT9oWf5+28nvmeUICjP99f4vr2d+qc7AVGDDtwRep6ifR43Yed9ERVmiITzw== + dependencies: + "@noble/hashes" "~1.5.0" + "@scure/base" "~1.1.8" + +"@scure/bip39@^1.4.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@scure/bip39/-/bip39-1.5.0.tgz#c8f9533dbd787641b047984356531d84485f19be" + integrity sha512-Dop+ASYhnrwm9+HA/HwXg7j2ZqM6yk2fyLWb5znexjctFY3+E+eU8cIWI0Pql0Qx4hPZCijlGq4OL71g+Uz30A== + dependencies: + "@noble/hashes" "~1.6.0" + "@scure/base" "~1.2.1" + "@shikijs/core@1.2.0": version "1.2.0" resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-1.2.0.tgz#c19d1a4d4807d31aa02e9d822aa13da873e6f2e7" @@ -5378,6 +5594,11 @@ resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.28.6.tgz#a3bdb108f9f8d4e2ba3163068dbe6ff55b905a81" integrity sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA== +"@tanstack/query-core@5.60.6": + version "5.60.6" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.60.6.tgz#0dd33fe231b0d18bf66d0c615b29899738300658" + integrity sha512-tI+k0KyCo1EBJ54vxK1kY24LWj673ujTydCZmzEZKAew4NqZzTaVQJEuaG1qKj2M03kUHN46rchLRd+TxVq/zQ== + "@tanstack/react-query@^5.22.2": version "5.28.6" resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.28.6.tgz#0d52b0a98a1d842debf9c65496e20a9981a23bc4" @@ -5385,6 +5606,13 @@ dependencies: "@tanstack/query-core" "5.28.6" +"@tanstack/react-query@^5.61.3": + version "5.61.3" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.61.3.tgz#0187b73b87adaeaed09f3d9717e35b507175fe23" + integrity sha512-c3Oz9KaCBapGkRewu7AJLhxE9BVqpMcHsd3KtFxSd7FSCu2qGwqfIN37zbSGoyk6Ix9LGZBNHQDPI6GpWABnmA== + dependencies: + "@tanstack/query-core" "5.60.6" + "@types/acorn@^4.0.0": version "4.0.6" resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.6.tgz#d61ca5480300ac41a7d973dd5b84d0a591154a22" @@ -6186,6 +6414,18 @@ "@walletconnect/modal" "2.6.2" cbw-sdk "npm:@coinbase/wallet-sdk@3.9.3" +"@wagmi/connectors@5.5.0": + version "5.5.0" + resolved "https://registry.yarnpkg.com/@wagmi/connectors/-/connectors-5.5.0.tgz#94bf6730dfea0032426230cd45b49183ccefb714" + integrity sha512-Ywzj6sYH3z2zp/n9C9sYGJj/uX9UMQQN5MQMKICnQIwkFmP9Uk78KiETvQHa0IHFusrrfviE2pr8XMsNNg9HTg== + dependencies: + "@coinbase/wallet-sdk" "4.2.3" + "@metamask/sdk" "0.30.1" + "@safe-global/safe-apps-provider" "0.18.4" + "@safe-global/safe-apps-sdk" "9.1.0" + "@walletconnect/ethereum-provider" "2.17.0" + cbw-sdk "npm:@coinbase/wallet-sdk@3.9.3" + "@wagmi/core@2.10.5": version "2.10.5" resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.10.5.tgz#9717ef118457dfb64550ca81a61efb66c2fbc4c3" @@ -6195,6 +6435,15 @@ mipd "0.0.5" zustand "4.4.1" +"@wagmi/core@2.15.0": + version "2.15.0" + resolved "https://registry.yarnpkg.com/@wagmi/core/-/core-2.15.0.tgz#a35c790b52096effa65e02ba89932f8aff23c1b9" + integrity sha512-nkvNbIYn52F0ZCUsF9wNu6mQ083XZGw2dUtT7aDTex+C+gvhDTUD7ef2nhEd5RdPuQmWMFpJGp4zvoykwSB1RQ== + dependencies: + eventemitter3 "5.0.1" + mipd "0.0.7" + zustand "5.0.0" + "@walletconnect/core@2.12.1": version "2.12.1" resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.12.1.tgz#e905e42f6c2a5117a1166c1a1d35e40aa98e76d3" @@ -6241,6 +6490,28 @@ lodash.isequal "4.5.0" uint8arrays "3.1.0" +"@walletconnect/core@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/core/-/core-2.17.0.tgz#bf490e85a4702eff0f7cf81ba0d3c1016dffff33" + integrity sha512-On+uSaCfWdsMIQsECwWHZBmUXfrnqmv6B8SXRRuTJgd8tUpEvBkLQH4X7XkSm3zW6ozEkQTCagZ2ox2YPn3kbw== + dependencies: + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/jsonrpc-ws-connection" "1.0.14" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.0" + "@walletconnect/utils" "2.17.0" + events "3.3.0" + lodash.isequal "4.5.0" + uint8arrays "3.1.0" + "@walletconnect/environment@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/environment/-/environment-1.0.1.tgz#1d7f82f0009ab821a2ba5ad5e5a7b8ae3b214cd7" @@ -6264,6 +6535,22 @@ "@walletconnect/utils" "2.13.0" events "3.3.0" +"@walletconnect/ethereum-provider@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.17.0.tgz#d74feaaed6180a6799e96760d7ee867ff3a083d2" + integrity sha512-b+KTAXOb6JjoxkwpgYQQKPUcTwENGmdEdZoIDLeRicUmZTn/IQKfkMoC2frClB4YxkyoVMtj1oMV2JAax+yu9A== + dependencies: + "@walletconnect/jsonrpc-http-connection" "1.0.8" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/modal" "2.7.0" + "@walletconnect/sign-client" "2.17.0" + "@walletconnect/types" "2.17.0" + "@walletconnect/universal-provider" "2.17.0" + "@walletconnect/utils" "2.17.0" + events "3.3.0" + "@walletconnect/ethereum-provider@^2.1.2", "@walletconnect/ethereum-provider@^2.4.7": version "2.12.1" resolved "https://registry.yarnpkg.com/@walletconnect/ethereum-provider/-/ethereum-provider-2.12.1.tgz#0085c6d9388e6f9322c81b698ad2653515f29e3f" @@ -6411,6 +6698,13 @@ dependencies: valtio "1.11.2" +"@walletconnect/modal-core@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-core/-/modal-core-2.7.0.tgz#73c13c3b7b0abf9ccdbac9b242254a86327ce0a4" + integrity sha512-oyMIfdlNdpyKF2kTJowTixZSo0PGlCJRdssUN/EZdA6H6v03hZnf09JnwpljZNfir2M65Dvjm/15nGrDQnlxSA== + dependencies: + valtio "1.11.2" + "@walletconnect/modal-ui@2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@walletconnect/modal-ui/-/modal-ui-2.6.2.tgz#fa57c087c57b7f76aaae93deab0f84bb68b59cf9" @@ -6421,6 +6715,16 @@ motion "10.16.2" qrcode "1.5.3" +"@walletconnect/modal-ui@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@walletconnect/modal-ui/-/modal-ui-2.7.0.tgz#dbbb7ee46a5a25f7d39db622706f2d197b268cbb" + integrity sha512-gERYvU7D7K1ANCN/8vUgsE0d2hnRemfAFZ2novm9aZBg7TEd/4EgB+AqbJ+1dc7GhOL6dazckVq78TgccHb7mQ== + dependencies: + "@walletconnect/modal-core" "2.7.0" + lit "2.8.0" + motion "10.16.2" + qrcode "1.5.3" + "@walletconnect/modal@2.6.2", "@walletconnect/modal@^2.6.2": version "2.6.2" resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.6.2.tgz#4b534a836f5039eeb3268b80be7217a94dd12651" @@ -6429,6 +6733,14 @@ "@walletconnect/modal-core" "2.6.2" "@walletconnect/modal-ui" "2.6.2" +"@walletconnect/modal@2.7.0": + version "2.7.0" + resolved "https://registry.yarnpkg.com/@walletconnect/modal/-/modal-2.7.0.tgz#55f969796d104cce1205f5f844d8f8438b79723a" + integrity sha512-RQVt58oJ+rwqnPcIvRFeMGKuXb9qkgSmwz4noF8JZGUym3gUAzVs+uW2NQ1Owm9XOJAV+sANrtJ+VoVq1ftElw== + dependencies: + "@walletconnect/modal-core" "2.7.0" + "@walletconnect/modal-ui" "2.7.0" + "@walletconnect/relay-api@1.0.10": version "1.0.10" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.10.tgz#5aef3cd07c21582b968136179aa75849dcc65499" @@ -6436,6 +6748,13 @@ dependencies: "@walletconnect/jsonrpc-types" "^1.0.2" +"@walletconnect/relay-api@1.0.11": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.11.tgz#80ab7ef2e83c6c173be1a59756f95e515fb63224" + integrity sha512-tLPErkze/HmC9aCmdZOhtVmYZq1wKfWTJtygQHoWtgg722Jd4homo54Cs4ak2RUFUZIGO2RsOpIcWipaua5D5Q== + dependencies: + "@walletconnect/jsonrpc-types" "^1.0.2" + "@walletconnect/relay-api@^1.0.9": version "1.0.9" resolved "https://registry.yarnpkg.com/@walletconnect/relay-api/-/relay-api-1.0.9.tgz#f8c2c3993dddaa9f33ed42197fc9bfebd790ecaf" @@ -6493,6 +6812,21 @@ "@walletconnect/utils" "2.13.0" events "3.3.0" +"@walletconnect/sign-client@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/sign-client/-/sign-client-2.17.0.tgz#efe811b1bb10082d964e2f0378aaa1b40f424503" + integrity sha512-sErYwvSSHQolNXni47L3Bm10ptJc1s1YoJvJd34s5E9h9+d3rj7PrhbiW9X82deN+Dm5oA8X9tC4xty1yIBrVg== + dependencies: + "@walletconnect/core" "2.17.0" + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.1.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.0" + "@walletconnect/utils" "2.17.0" + events "3.3.0" + "@walletconnect/time@1.0.2", "@walletconnect/time@^1.0.2": version "1.0.2" resolved "https://registry.yarnpkg.com/@walletconnect/time/-/time-1.0.2.tgz#6c5888b835750ecb4299d28eecc5e72c6d336523" @@ -6524,6 +6858,18 @@ "@walletconnect/logger" "2.1.2" events "3.3.0" +"@walletconnect/types@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/types/-/types-2.17.0.tgz#20eda5791e3172f8ab9146caa3f317701d4b3232" + integrity sha512-i1pn9URpvt9bcjRDkabuAmpA9K7mzyKoLJlbsAujRVX7pfaG7wur7u9Jz0bk1HxvuABL5LHNncTnVKSXKQ5jZA== + dependencies: + "@walletconnect/events" "1.0.1" + "@walletconnect/heartbeat" "1.2.2" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/keyvaluestorage" "1.1.1" + "@walletconnect/logger" "2.1.2" + events "3.3.0" + "@walletconnect/universal-provider@2.12.1": version "2.12.1" resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.12.1.tgz#c092a123a7d1e5e0462a667bff5e3908d90d928f" @@ -6554,6 +6900,21 @@ "@walletconnect/utils" "2.13.0" events "3.3.0" +"@walletconnect/universal-provider@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/universal-provider/-/universal-provider-2.17.0.tgz#c9d4bbd9b8f0e41b500b2488ccbc207dc5f7a170" + integrity sha512-d3V5Be7AqLrvzcdMZSBS8DmGDRdqnyLk1DWmRKAGgR6ieUWykhhUKlvfeoZtvJrIXrY7rUGYpH1X41UtFkW5Pw== + dependencies: + "@walletconnect/jsonrpc-http-connection" "1.0.8" + "@walletconnect/jsonrpc-provider" "1.0.14" + "@walletconnect/jsonrpc-types" "1.0.4" + "@walletconnect/jsonrpc-utils" "1.0.8" + "@walletconnect/logger" "2.1.2" + "@walletconnect/sign-client" "2.17.0" + "@walletconnect/types" "2.17.0" + "@walletconnect/utils" "2.17.0" + events "3.3.0" + "@walletconnect/utils@2.12.1": version "2.12.1" resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.12.1.tgz#5fced674e0a732eb62f30391943e79abbf3d5d1f" @@ -6594,6 +6955,28 @@ query-string "7.1.3" uint8arrays "3.1.0" +"@walletconnect/utils@2.17.0": + version "2.17.0" + resolved "https://registry.yarnpkg.com/@walletconnect/utils/-/utils-2.17.0.tgz#02b3af0b80d0c1a994d692d829d066271b04d071" + integrity sha512-1aeQvjwsXy4Yh9G6g2eGmXrEl+BzkNjHRdCrGdMYqFTFa8ROEJfTGsSH3pLsNDlOY94CoBUvJvM55q/PMoN/FQ== + dependencies: + "@stablelib/chacha20poly1305" "1.0.1" + "@stablelib/hkdf" "1.0.1" + "@stablelib/random" "1.0.2" + "@stablelib/sha256" "1.0.1" + "@stablelib/x25519" "1.0.3" + "@walletconnect/relay-api" "1.0.11" + "@walletconnect/relay-auth" "1.0.4" + "@walletconnect/safe-json" "1.0.2" + "@walletconnect/time" "1.0.2" + "@walletconnect/types" "2.17.0" + "@walletconnect/window-getters" "1.0.1" + "@walletconnect/window-metadata" "1.0.1" + detect-browser "5.3.0" + elliptic "^6.5.7" + query-string "7.1.3" + uint8arrays "3.1.0" + "@walletconnect/window-getters@1.0.1", "@walletconnect/window-getters@^1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@walletconnect/window-getters/-/window-getters-1.0.1.tgz#f36d1c72558a7f6b87ecc4451fc8bd44f63cbbdc" @@ -6737,6 +7120,11 @@ abitype@1.0.5: resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.5.tgz#29d0daa3eea867ca90f7e4123144c1d1270774b6" integrity sha512-YzDhti7cjlfaBhHutMaboYB21Ha3rXR9QTkNJFzYC4kC8YclaiwPBBBJY8ejFdu2wnJeZCVZSMlQJ7fi8S6hsw== +abitype@1.0.6, abitype@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/abitype/-/abitype-1.0.6.tgz#76410903e1d88e34f1362746e2d407513c38565b" + integrity sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A== + abort-controller@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" @@ -8005,6 +8393,11 @@ combined-stream@^1.0.8: dependencies: delayed-stream "~1.0.0" +comlink@^4.4.2: + version "4.4.2" + resolved "https://registry.yarnpkg.com/comlink/-/comlink-4.4.2.tgz#cbbcd82742fbebc06489c28a183eedc5c60a2bca" + integrity sha512-OxGdvBmJuNKSCMO4NTl1L47VRp6xn2wG4F/2hYzB6tiCb709otOxtEYCSvK80PtjODfXXZu8ds+Nw5kVCjqd2g== + comma-separated-tokens@^2.0.0: version "2.0.3" resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" @@ -8762,6 +9155,16 @@ eciesjs@^0.3.15: futoin-hkdf "^1.5.3" secp256k1 "^5.0.0" +eciesjs@^0.4.8: + version "0.4.12" + resolved "https://registry.yarnpkg.com/eciesjs/-/eciesjs-0.4.12.tgz#0ce482454953592e07b79b4824751f3b5c508b56" + integrity sha512-DGejvMCihsRAmKRFQiL6KZDE34vWVd0gvXlykFq1aEzJy/rD65AVyAIUZKZOvgvaP9ATQRcHGEZV5DfgrgjA4w== + dependencies: + "@ecies/ciphers" "^0.2.1" + "@noble/ciphers" "^1.0.0" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" @@ -8798,6 +9201,19 @@ elliptic@^6.5.4: minimalistic-assert "^1.0.1" minimalistic-crypto-utils "^1.0.1" +elliptic@^6.5.7: + version "6.6.1" + resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.6.1.tgz#3b8ffb02670bf69e382c7f65bf524c97c5405c06" + integrity sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g== + dependencies: + bn.js "^4.11.9" + brorand "^1.1.0" + hash.js "^1.0.0" + hmac-drbg "^1.0.1" + inherits "^2.0.4" + minimalistic-assert "^1.0.1" + minimalistic-crypto-utils "^1.0.1" + emittery@^0.13.1: version "0.13.1" resolved "https://registry.yarnpkg.com/emittery/-/emittery-0.13.1.tgz#c04b8c3457490e0847ae51fced3af52d338e3dad" @@ -10906,6 +11322,13 @@ i18next@22.5.1: dependencies: "@babel/runtime" "^7.20.6" +i18next@23.11.5: + version "23.11.5" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.11.5.tgz#d71eb717a7e65498d87d0594f2664237f9e361ef" + integrity sha512-41pvpVbW9rhZPk5xjCX2TPJi2861LEig/YRhUkY+1FQ2IQPS0bKUDYnEqY8XPPbB48h1uIwLnP9iiEfuSl20CA== + dependencies: + "@babel/runtime" "^7.23.2" + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -11475,6 +11898,11 @@ isows@1.0.4: resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.4.tgz#810cd0d90cc4995c26395d2aa4cfa4037ebdf061" integrity sha512-hEzjY+x9u9hPmBom9IIAqdJCwNLax+xrPb51vEPpERoFlIxgmZcHzsT5jKG06nvInKOBGvReAVz80Umed5CczQ== +isows@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/isows/-/isows-1.0.6.tgz#0da29d706fa51551c663c627ace42769850f86e7" + integrity sha512-lPHCayd40oW98/I0uvgaHKWCSvkzY27LjWLbtzOm64yQ+G3Q5npjjbdppU65iZXkK1Zt+kH9pfegli0AYfwYYw== + istanbul-lib-coverage@^3.0.0, istanbul-lib-coverage@^3.2.0: version "3.2.2" resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz#2d166c4b0644d43a39f04bf6c2edd1e585f31756" @@ -14020,6 +14448,11 @@ mipd@0.0.5: dependencies: viem "^1.1.4" +mipd@0.0.7: + version "0.0.7" + resolved "https://registry.yarnpkg.com/mipd/-/mipd-0.0.7.tgz#bb5559e21fa18dc3d9fe1c08902ef14b7ce32fd9" + integrity sha512-aAPZPNDQ3uMTdKbuO2YmAw2TxLHO0moa4YKAyETM/DTj5FloZo+a+8tU+iv4GmW+sOxKLSRwcSFuczk+Cpt6fg== + mixme@^0.5.1: version "0.5.10" resolved "https://registry.yarnpkg.com/mixme/-/mixme-0.5.10.tgz#d653b2984b75d9018828f1ea333e51717ead5f51" @@ -14646,6 +15079,32 @@ outdent@^0.8.0: resolved "https://registry.yarnpkg.com/outdent/-/outdent-0.8.0.tgz#2ebc3e77bf49912543f1008100ff8e7f44428eb0" integrity sha512-KiOAIsdpUTcAXuykya5fnVVT+/5uS0Q1mrkRHcF89tpieSmY33O/tmc54CqwA+bfhbtEfZUNLHaPUiB9X3jt1A== +ox@0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.2.tgz#0f791be2ccabeaf4928e6d423498fe1c8094e560" + integrity sha512-ak/8K0Rtphg9vnRJlbOdaX9R7cmxD2MiSthjWGaQdMk3D7hrAlDoM+6Lxn7hN52Za3vrXfZ7enfke/5WjolDww== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + +ox@^0.1.6: + version "0.1.8" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.1.8.tgz#0ba58eb6f2471c607959c40fa39b2fa5c4f4f7e5" + integrity sha512-GJl6uKXxhPq/XgyvAnIokGuGU/pt9CU8reRJjzi4a02HOpLc2CEXXD4bRCITFFAzdRqHj3DQ6GDS7PlCytPM/A== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + p-filter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" @@ -15119,6 +15578,11 @@ preact@^10.16.0: resolved "https://registry.yarnpkg.com/preact/-/preact-10.20.0.tgz#191c10a2ee3b9fca1a7ded6375266266380212f6" integrity sha512-wU7iZw2BjsaKDal3pDRDy/HpPB6cuFOnVUCcw9aIPKG98+ZrXx3F+szkos8BVME5bquyKDKvRlOJFG8kMkcAbg== +preact@^10.24.2: + version "10.25.0" + resolved "https://registry.yarnpkg.com/preact/-/preact-10.25.0.tgz#22a1c93ce97336c5d01d74f363433ab0cd5cde64" + integrity sha512-6bYnzlLxXV3OSpUxLdaxBmE7PMOu0aR3pG6lryK/0jmvcDFPlcXGQAt5DpK3RITWiDrfYZRI0druyaK/S9kYLg== + preferred-pm@^3.0.0: version "3.1.3" resolved "https://registry.yarnpkg.com/preferred-pm/-/preferred-pm-3.1.3.tgz#4125ea5154603136c3b6444e5f5c94ecf90e4916" @@ -17973,6 +18437,13 @@ use-sync-external-store@1.2.0: resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.0.tgz#7dbefd6ef3fe4e767a0cf5d7287aacfb5846928a" integrity sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA== +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + utf-8-validate@^6.0.3: version "6.0.3" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-6.0.3.tgz#7d8c936d854e86b24d1d655f138ee27d2636d777" @@ -18131,6 +18602,21 @@ viem@^1.0.0, viem@^1.1.4, viem@^1.12.2: isows "1.0.3" ws "8.13.0" +viem@^2.1.1, viem@^2.21.50: + version "2.21.50" + resolved "https://registry.yarnpkg.com/viem/-/viem-2.21.50.tgz#a645a7fb4a017c644712b905e03ab533b7e24ea7" + integrity sha512-WHB8NmkaForODuSALb0Ai3E296aEigzYSE+pzB9Y0cTNJeiZT8rpkdxxUFYfjwFMnPkz2tivqrSpuw3hO5TH6w== + dependencies: + "@noble/curves" "1.6.0" + "@noble/hashes" "1.5.0" + "@scure/bip32" "1.5.0" + "@scure/bip39" "1.4.0" + abitype "1.0.6" + isows "1.0.6" + ox "0.1.2" + webauthn-p256 "0.0.10" + ws "8.18.0" + viem@^2.13.7: version "2.13.7" resolved "https://registry.yarnpkg.com/viem/-/viem-2.13.7.tgz#c1153c02f7ffaf0263d784fc1d4e4ffa3f66c24a" @@ -18323,6 +18809,15 @@ vocs@1.0.0-alpha.46: unist-util-visit "^5.0.0" vite "^5.0.2" +wagmi@^2.13.0: + version "2.13.0" + resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.13.0.tgz#c5d3c608604c99b03ce41eefec0f65da074d12d2" + integrity sha512-afgHaOYXkji0QvDUNCcwIWYvzjwcDtoAPRqSBfGq9rj4v2SCztv/sYz0C43b5NoazI0LoKar8ykx8LEr3Euofg== + dependencies: + "@wagmi/connectors" "5.5.0" + "@wagmi/core" "2.15.0" + use-sync-external-store "1.2.0" + wagmi@^2.9.10: version "2.9.10" resolved "https://registry.yarnpkg.com/wagmi/-/wagmi-2.9.10.tgz#ea0f87eb025afea171306acd6e9a6f332de1c65c" @@ -18365,6 +18860,14 @@ web-vitals@^3.0.4: resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.5.2.tgz#5bb58461bbc173c3f00c2ddff8bfe6e680999ca9" integrity sha512-c0rhqNcHXRkY/ogGDJQxZ9Im9D19hDihbzSQJrsioex+KnFgmMzBiy57Z1EjkhX/+OjyBpclDCzz2ITtjokFmg== +webauthn-p256@0.0.10: + version "0.0.10" + resolved "https://registry.yarnpkg.com/webauthn-p256/-/webauthn-p256-0.0.10.tgz#877e75abe8348d3e14485932968edf3325fd2fdd" + integrity sha512-EeYD+gmIT80YkSIDb2iWq0lq2zbHo1CxHlQTeJ+KkCILWpVy3zASH3ByD4bopzfk0uCwXxLqKGLqp2W4O28VFA== + dependencies: + "@noble/curves" "^1.4.0" + "@noble/hashes" "^1.4.0" + "webextension-polyfill@>=0.10.0 <1.0", webextension-polyfill@^0.10.0: version "0.10.0" resolved "https://registry.yarnpkg.com/webextension-polyfill/-/webextension-polyfill-0.10.0.tgz#ccb28101c910ba8cf955f7e6a263e662d744dbb8" @@ -18600,6 +19103,11 @@ ws@8.17.1: resolved "https://registry.yarnpkg.com/ws/-/ws-8.17.1.tgz#9293da530bb548febc95371d90f9c878727d919b" integrity sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ== +ws@8.18.0: + version "8.18.0" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.0.tgz#0d7505a6eafe2b0e712d232b42279f53bc289bbc" + integrity sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw== + ws@^6.2.2: version "6.2.3" resolved "https://registry.yarnpkg.com/ws/-/ws-6.2.3.tgz#ccc96e4add5fd6fedbc491903075c85c5a11d9ee" @@ -18784,6 +19292,11 @@ zustand@4.4.1: dependencies: use-sync-external-store "1.2.0" +zustand@5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0.tgz#71f8aaecf185592a3ba2743d7516607361899da9" + integrity sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ== + zwitch@^2.0.0: version "2.0.4" resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" From fb055d2a6d279d9fe7f299daf1e737492a29fdee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 27 Nov 2024 10:32:06 +0100 Subject: [PATCH 02/88] fix: cast action message response definition --- packages/frames.js/src/core/cast-actions.ts | 10 +++++++++- packages/frames.js/src/core/types.ts | 2 ++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/frames.js/src/core/cast-actions.ts b/packages/frames.js/src/core/cast-actions.ts index 9df6c1225..205982c67 100644 --- a/packages/frames.js/src/core/cast-actions.ts +++ b/packages/frames.js/src/core/cast-actions.ts @@ -46,6 +46,9 @@ export function composerAction( } satisfies ComposerActionResponse); } +/** + * @see https://docs.farcaster.xyz/reference/actions/spec#frame-response-type + */ export function castActionFrame(frameUrl: string): Response { return Response.json({ type: "frame", @@ -53,9 +56,14 @@ export function castActionFrame(frameUrl: string): Response { } satisfies CastActionFrameResponse); } -export function castActionMessage(message: string): Response { +/** + * @see https://docs.farcaster.xyz/reference/actions/spec#message-response-type + */ +export function castActionMessage(message: string, link?: string): Response { return Response.json({ + type: "message", message, + link, } satisfies CastActionMessageResponse); } diff --git a/packages/frames.js/src/core/types.ts b/packages/frames.js/src/core/types.ts index 329916bfa..489a62dd3 100644 --- a/packages/frames.js/src/core/types.ts +++ b/packages/frames.js/src/core/types.ts @@ -410,7 +410,9 @@ export type ErrorMessageResponse = { }; export type CastActionMessageResponse = { + type: "message"; message: string; + link?: string; }; export type CastActionFrameResponse = { From bd19f001011a4e6185998dccf5b10d4c2d5bdcd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 27 Nov 2024 10:32:33 +0100 Subject: [PATCH 03/88] feat: basic frames v2 parsing --- packages/frames.js/package.json | 3 +- .../src/frame-parsers/farcasterV2.ts | 268 ++++++++++++++++++ packages/frames.js/src/frame-parsers/types.ts | 43 ++- packages/frames.js/src/getFrameFlattened.ts | 17 +- packages/frames.js/src/getFrameHtml.ts | 25 +- .../frames.js/src/parseFramesWithReports.ts | 10 + packages/frames.js/src/types.ts | 55 +++- yarn.lock | 5 + 8 files changed, 411 insertions(+), 15 deletions(-) create mode 100644 packages/frames.js/src/frame-parsers/farcasterV2.ts diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index 9601a6fb3..3c5e0fbe6 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -392,6 +392,7 @@ "@vercel/og": "^0.6.3", "cheerio": "^1.0.0-rc.12", "protobufjs": "^7.2.6", - "viem": "^2.7.8" + "viem": "^2.7.8", + "type-fest": "^4.28.1" } } diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts new file mode 100644 index 000000000..31fbd8d1a --- /dev/null +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -0,0 +1,268 @@ +import type { CheerioAPI } from "cheerio"; +import type { FrameV2 } from "../types"; +import { getMetaTag } from "./utils"; +import type { ParseResultFramesV2, ParsedFrameV2, Reporter } from "./types"; + +type Options = { + reporter: Reporter; +}; + +// @todo add optional frame manifest validation? +// @todo add a way to turn on only some validations, for example manifest on, all the rest like image size off +export function parseFarcasterFrameV2( + $: CheerioAPI, + { reporter }: Options +): ParseResultFramesV2 { + const embed = getMetaTag($, "fc:frame"); + + if (!embed) { + reporter.error("fc:frame", 'Missing required meta tag "fc:frame"'); + + return { + status: "failure", + frame: {}, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; + } + + let parsedJSON: unknown; + const parsedFrame: ParsedFrameV2 = {}; + + try { + parsedJSON = JSON.parse(embed); + } catch (error) { + reporter.error( + "fc:frame", + "Failed to parse FrameEmbed it is not a valid JSON value" + ); + + return { + status: "failure", + frame: {}, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; + } + + if (typeof parsedJSON !== "object") { + reporter.error("fc:frame", "FrameEmbed must be an object"); + + return { + status: "failure", + frame: {}, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; + } + + if (parsedJSON === null) { + reporter.error("fc:frame", "FrameEmbed must not be null"); + + return { + status: "failure", + frame: {}, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; + } + + if (!("imageUrl" in parsedJSON)) { + reporter.error("fc:frame", 'Missing required key "imageUrl" in FrameEmbed'); + } else if (typeof parsedJSON.imageUrl !== "string") { + reporter.error("fc:frame", 'Key "imageUrl" in FrameEmbed must be a string'); + } else if (!URL.canParse(parsedJSON.imageUrl)) { + reporter.error( + "fc:frame", + 'Key "imageUrl" in FrameEmbed must be a valid URL' + ); + } else { + parsedFrame.imageUrl = parsedJSON.imageUrl; + } + + // @todo add optional validation for frame image size + + if (!("button" in parsedJSON)) { + reporter.error("fc:frame", 'Missing required key "button" in FrameEmbed'); + } else { + validateFrameButton(parsedJSON.button, reporter); + } + + if (reporter.hasErrors()) { + return { + status: "failure", + frame: parsedFrame, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; + } + + return { + status: "success", + frame: parsedFrame as unknown as FrameV2, + reports: reporter.toObject(), + specification: "farcaster_v2", + }; +} + +function validateFrameButton( + parsedValue: unknown, + reporter: Reporter +): ParsedFrameV2["button"] { + if (typeof parsedValue !== "object") { + reporter.error("fc:frame", 'Key "button" in FrameEmbed must be an object'); + + return {}; + } + + if (parsedValue === null) { + reporter.error("fc:frame", 'Key "button" in FrameEmbed must not be null'); + + return {}; + } + + const button: ParsedFrameV2["button"] = {}; + + if (!("title" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "title" in FrameEmbed.button' + ); + } else if (typeof parsedValue.title !== "string") { + reporter.error( + "fc:frame", + 'Key "title" in FrameEmbed.button must be a string' + ); + } else { + button.title = parsedValue.title; + } + + if (!("action" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "action" in FrameEmbed.button' + ); + } else { + validateFrameButtonAction(parsedValue.action, reporter); + } + + return button; +} + +function validateFrameButtonAction( + parsedValue: unknown, + reporter: Reporter +): NonNullable["action"] { + if (typeof parsedValue !== "object") { + reporter.error( + "fc:frame", + 'Key "action" in FrameEmbed.button must be an object' + ); + + return {}; + } + + if (parsedValue === null) { + reporter.error( + "fc:frame", + 'Key "action" in FrameEmbed.button must not be null' + ); + + return {}; + } + + const action: NonNullable["action"] = {}; + + if (!("name" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "name" in FrameEmbed.button.action' + ); + } else if (typeof parsedValue.name !== "string") { + reporter.error( + "fc:frame", + 'Key "name" in FrameEmbed.button.action must be a string' + ); + } else { + action.name = parsedValue.name; + } + + if (!("type" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "type" in FrameEmbed.button.action' + ); + } else if (typeof parsedValue.type !== "string") { + reporter.error( + "fc:frame", + 'Key "type" in FrameEmbed.button.action must be a string' + ); + } else if (parsedValue.type !== "launch") { + reporter.error( + "fc:frame", + 'Key "type" in FrameEmbed.button.action must be "launch"' + ); + } else { + action.type = parsedValue.type; + } + + if (!("url" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "url" in FrameEmbed.button.action' + ); + } else if (typeof parsedValue.url !== "string") { + reporter.error( + "fc:frame", + 'Key "url" in FrameEmbed.button.action must be a string' + ); + } else if (!URL.canParse(parsedValue.url)) { + reporter.error( + "fc:frame", + 'Key "url" in FrameEmbed.button.action must be a valid URL' + ); + } else { + action.url = parsedValue.url; + } + + // @todo optionaly validate splashImage dimensions and file size + if (!("splashImage" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "splashImage" in FrameEmbed.button.action' + ); + } else if (typeof parsedValue.splashImage !== "string") { + reporter.error( + "fc:frame", + 'Key "splashImage" in FrameEmbed.button.action must be a string' + ); + } else if (!URL.canParse(parsedValue.splashImage)) { + reporter.error( + "fc:frame", + 'Key "splashImage" in FrameEmbed.button.action must be a valid URL' + ); + } else { + action.splashImage = parsedValue.splashImage; + } + + if (!("splashBackgroundColor" in parsedValue)) { + reporter.error( + "fc:frame", + 'Missing required key "splashBackgroundColor" in FrameEmbed.button.action' + ); + } else if (typeof parsedValue.splashBackgroundColor !== "string") { + reporter.error( + "fc:frame", + 'Key "splashBackgroundColor" in FrameEmbed.button.action must be a string' + ); + } else if (!/^#[0-9a-fA-F]{6,8}$/.test(parsedValue.splashBackgroundColor)) { + reporter.error( + "fc:frame", + 'Key "splashBackgroundColor" in FrameEmbed.button.action must be a valid hex color' + ); + } else { + action.splashBackgroundColor = parsedValue.splashBackgroundColor; + } + + return action; +} diff --git a/packages/frames.js/src/frame-parsers/types.ts b/packages/frames.js/src/frame-parsers/types.ts index f47886fae..66e910e5a 100644 --- a/packages/frames.js/src/frame-parsers/types.ts +++ b/packages/frames.js/src/frame-parsers/types.ts @@ -1,6 +1,10 @@ -import type { Frame } from "../types"; +import type { PartialDeep } from "type-fest"; +import type { Frame, FrameV2 } from "../types"; -export type SupportedParsingSpecification = "farcaster" | "openframes"; +export type SupportedParsingSpecification = + | "farcaster" + | "farcaster_v2" + | "openframes"; export interface Reporter { error: (key: string, message: unknown, source?: ParsingReportSource) => void; @@ -33,6 +37,8 @@ export type ParsedFrame = { title?: string; }; +export type ParsedFrameV2 = PartialDeep; + export type ParsingReportSource = SupportedParsingSpecification; export type ParsingReportLevel = "error" | "warning"; @@ -51,7 +57,7 @@ export type ParseResult = * Reports contain only warnings that should not have any impact on the frame's functionality. */ reports: Record; - specification: SupportedParsingSpecification; + specification: "farcaster" | "openframes"; } | { status: "failure"; @@ -60,7 +66,27 @@ export type ParseResult = * Reports contain warnings and errors that should be addressed before the frame can be used. */ reports: Record; - specification: SupportedParsingSpecification; + specification: "farcaster" | "openframes"; + }; + +export type ParseResultFramesV2 = + | { + status: "success"; + frame: FrameV2; + /** + * Reports contain only warnings that should not have any impact on the frame's functionality. + */ + reports: Record; + specification: "farcaster_v2"; + } + | { + status: "failure"; + frame: PartialDeep; + /** + * Reports contain warnings and errors that should be addressed before the frame can be used. + */ + reports: Record; + specification: "farcaster_v2"; }; export type ParsedFrameworkDetails = { @@ -73,9 +99,14 @@ export type ParsedFrameworkDetails = { }; }; -export type ParseResultWithFrameworkDetails = ParseResult & +export type ParseResultWithFrameworkDetails = ( + | ParseResult + | ParseResultFramesV2 +) & ParsedFrameworkDetails; export type ParseFramesWithReportsResult = { - [K in SupportedParsingSpecification]: ParseResultWithFrameworkDetails; + farcaster: ParseResultWithFrameworkDetails; + farcaster_v2: ParseResultFramesV2; + openframes: ParseResultWithFrameworkDetails; }; diff --git a/packages/frames.js/src/getFrameFlattened.ts b/packages/frames.js/src/getFrameFlattened.ts index 04e02c537..2ef15d109 100644 --- a/packages/frames.js/src/getFrameFlattened.ts +++ b/packages/frames.js/src/getFrameFlattened.ts @@ -1,5 +1,6 @@ import { version as framesjsVersion } from "../package.json"; -import type { Frame, FrameFlattened } from "./types"; +import type { ParsedFrameV2 } from "./frame-parsers"; +import type { Frame, FrameFlattened, FrameV2Flattened } from "./types"; export function getFrameFlattened( frame: Frame, @@ -96,3 +97,17 @@ export function getFrameFlattened( return metadata; } + +/** + * Formats a Frame v2 and formats it as an intermediate step before rendering as html + */ +export function getFrameV2Flattened( + frame: ParsedFrameV2, + overrides?: Partial +): Partial { + return { + "fc:frame": JSON.stringify(frame), + [`frames.js:version`]: framesjsVersion, + ...overrides, + }; +} diff --git a/packages/frames.js/src/getFrameHtml.ts b/packages/frames.js/src/getFrameHtml.ts index 3eacc9eec..267af3dcf 100644 --- a/packages/frames.js/src/getFrameHtml.ts +++ b/packages/frames.js/src/getFrameHtml.ts @@ -1,6 +1,7 @@ import { DEFAULT_FRAME_TITLE } from "./constants"; -import { getFrameFlattened } from "./getFrameFlattened"; -import type { Frame, FrameFlattened } from "./types"; +import type { ParsedFrameV2 } from "./frame-parsers"; +import { getFrameFlattened, getFrameV2Flattened } from "./getFrameFlattened"; +import type { Frame, FrameFlattened, FrameV2Flattened } from "./types"; import { escapeHtmlAttributeValue } from "./utils"; export interface GetFrameHtmlOptions { @@ -67,3 +68,23 @@ export function getFrameHtmlHead( return tags.join(""); } + +/** + * Formats a Frame v2 ready to be included in a of an html string + */ +export function getFrameV2HtmlHead( + frame: ParsedFrameV2, + overrides?: Partial +): string { + const flattened = getFrameV2Flattened(frame, overrides); + + const tagStrings = Object.entries(flattened) + .map(([key, value]) => { + return value + ? `` + : null; + }) + .filter(Boolean) as string[]; + + return tagStrings.join(""); +} diff --git a/packages/frames.js/src/parseFramesWithReports.ts b/packages/frames.js/src/parseFramesWithReports.ts index c9932a8d9..ef6a404df 100644 --- a/packages/frames.js/src/parseFramesWithReports.ts +++ b/packages/frames.js/src/parseFramesWithReports.ts @@ -7,6 +7,7 @@ import type { import { parseFarcasterFrame } from "./frame-parsers/farcaster"; import { parseOpenFramesFrame } from "./frame-parsers/open-frames"; import { FRAMESJS_DEBUG_INFO_IMAGE_KEY } from "./constants"; +import { parseFarcasterFrameV2 } from "./frame-parsers/farcasterV2"; type ParseFramesWithReportsOptions = { html: string; @@ -33,6 +34,7 @@ export function parseFramesWithReports({ fromRequestMethod = "GET", }: ParseFramesWithReportsOptions): ParseFramesWithReportsResult { const farcasterReporter = createReporter("farcaster"); + const farcasterV2Reporter = createReporter("farcaster_v2"); const openFramesReporter = createReporter("openframes"); const document = loadDocument(html); @@ -42,6 +44,10 @@ export function parseFramesWithReports({ fromRequestMethod, }); + const farcasterV2 = parseFarcasterFrameV2(document, { + reporter: farcasterV2Reporter, + }); + const framesVersion = document( "meta[name='frames.js:version'], meta[property='frames.js:version']" ).attr("content"); @@ -73,6 +79,10 @@ export function parseFramesWithReports({ ...farcaster, ...frameworkDetails, }, + farcaster_v2: { + ...farcasterV2, + ...frameworkDetails, + }, openframes: { ...openframes, ...frameworkDetails, diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index d87bfa96e..cfe735e5f 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -34,16 +34,55 @@ export type Frame = { title?: string; }; +export type FrameV2 = { + /** + * A URL to image with 1.91:1 aspect ratio smaller than 10MB. + */ + imageUrl: string; + button: { + title: string; + action: { + /** + * Must be 'launch' + */ + type: "launch"; + /** + * App name + */ + name: string; + /** + * URL to App icon, must be 200x200px, less than 1MB + */ + icon: string; + /** + * App launch URL + */ + url: string; + /** + * URL to splash image, must 200x200px, less than 1MB + */ + splashImage: string; + /** + * Hex color code for splash background + */ + splashBackgroundColor: string; + }; + }; +}; + export type ActionButtonType = "post" | "post_redirect" | "link"; +type FrameJSOptionalStringKeys = + | "frames.js:version" + | "frames.js:debug-info:image"; + type FrameOptionalStringKeys = | "fc:frame:image:aspect_ratio" | "fc:frame:input:text" | "fc:frame:state" | "fc:frame:post_url" | keyof OpenFramesProperties - | "frames.js:version" - | "frames.js:debug-info:image"; + | FrameJSOptionalStringKeys; type FrameOptionalActionButtonTypeKeys = `fc:frame:button:${ | 1 | 2 @@ -62,8 +101,8 @@ type MapFrameOptionalKeyToValueType = K extends FrameOptionalStringKeys ? string | undefined : K extends FrameOptionalActionButtonTypeKeys - ? ActionButtonType | undefined - : string | undefined; + ? ActionButtonType | undefined + : string | undefined; type FrameRequiredProperties = { "fc:frame": FrameVersion; @@ -98,6 +137,12 @@ export type FrameFlattened = FrameRequiredProperties & { | FrameOptionalButtonStringKeys]?: MapFrameOptionalKeyToValueType; }; +export type FrameV2Flattened = { + "fc:frame": string; +} & { + [K in FrameJSOptionalStringKeys]?: MapFrameOptionalKeyToValueType; +}; + export interface FrameButtonLink { action: "link"; /** required for action type 'link' */ @@ -151,7 +196,7 @@ export type ActionIndex = 1 | 2 | 3 | 4; export type FrameButtonsType = FrameButton[]; export type AddressReturnType< - Options extends { fallbackToCustodyAddress?: boolean } | undefined + Options extends { fallbackToCustodyAddress?: boolean } | undefined, > = Options extends { fallbackToCustodyAddress: true } ? `0x${string}` : `0x${string}` | null; diff --git a/yarn.lock b/yarn.lock index 7c6774d87..31372253c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18024,6 +18024,11 @@ type-fest@^0.8.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d" integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA== +type-fest@^4.28.1: + version "4.28.1" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-4.28.1.tgz#5ae370169c829303012d4e2e1f568b427c1f37f7" + integrity sha512-LO/+yb3mf46YqfUC7QkkoAlpa7CTYh//V1Xy9+NQ+pKqDqXIq0NTfPfQRwFfCt+if4Qkwb9gzZfsl6E5TkXZGw== + type-is@~1.6.18: version "1.6.18" resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131" From 53ca37d3f8c4d6c6d4ff51f8b850693ce23d776a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:26:36 +0100 Subject: [PATCH 04/88] fix: do not cause infinite render cycle when using initialPendingExtra --- packages/render/src/unstable-use-frame-state.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/render/src/unstable-use-frame-state.ts b/packages/render/src/unstable-use-frame-state.ts index 45029b0ef..a170dcefa 100644 --- a/packages/render/src/unstable-use-frame-state.ts +++ b/packages/render/src/unstable-use-frame-state.ts @@ -272,6 +272,7 @@ export function useFrameState< TExtraMesssage >(resolveSpecificationRef) ); + const initialPendingExtraRef = useFreshRef(initialPendingExtra); const [state, dispatch] = useReducer( reducerRef.current, [initialParseResult, initialFrameUrl, initialPendingExtra] as const, @@ -458,13 +459,13 @@ export function useFrameState< action: "RESET_INITIAL_FRAME", homeframeUrl: arg.homeframeUrl, parseResult: arg.parseResult, - extra: (initialPendingExtra ?? {}) as TExtraDone, + extra: (initialPendingExtraRef.current ?? {}) as TExtraDone, }); } }, }; }, [ - initialPendingExtra, + initialPendingExtraRef, resolveDoneExtraRef, resolveDoneRedirectExtraRef, resolveDoneWithErrorMessageExtraRef, From 521d26c4c8b3c8bbcd4b41d05998dd4008d8a3e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:28:25 +0100 Subject: [PATCH 05/88] feat: basic frames v2 support --- packages/render/package.json | 32 ++- packages/render/src/collapsed-frame-ui.tsx | 23 +- packages/render/src/frame-ui.tsx | 24 ++- packages/render/src/next/validators.ts | 2 +- packages/render/src/types.ts | 4 +- packages/render/src/ui/frame.base.tsx | 234 ++++++++++++++++----- packages/render/src/ui/types.ts | 106 ++++++++-- packages/render/src/ui/utils.ts | 71 ++++++- packages/render/src/use-frame-stack.ts | 2 +- 9 files changed, 391 insertions(+), 107 deletions(-) diff --git a/packages/render/package.json b/packages/render/package.json index efe34f3cc..5ff2536df 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -100,6 +100,26 @@ "default": "./dist/ui/index.cjs" } }, + "./ui/utils": { + "import": { + "types": "./dist/ui/utils.d.ts", + "default": "./dist/ui/utils.js" + }, + "require": { + "types": "./dist/ui/utils.d.cts", + "default": "./dist/ui/utils.cjs" + } + }, + "./ui/types": { + "import": { + "types": "./dist/ui/types.d.ts", + "default": "./dist/ui/types.js" + }, + "require": { + "types": "./dist/ui/types.d.cts", + "default": "./dist/ui/types.cjs" + } + }, "./use-fetch-frame": { "import": { "types": "./dist/use-fetch-frame.d.ts", @@ -130,6 +150,16 @@ "default": "./dist/use-frame.cjs" } }, + "./use-cast-action": { + "import": { + "types": "./dist/use-cast-action.d.ts", + "default": "./dist/use-cast-action.js" + }, + "require": { + "types": "./dist/use-cast-action.d.cts", + "default": "./dist/use-cast-action.cjs" + } + }, "./use-composer-action": { "import": { "types": "./dist/use-composer-action.d.ts", @@ -288,4 +318,4 @@ "frames.js": "^0.20.0", "zod": "^3.23.8" } -} \ No newline at end of file +} diff --git a/packages/render/src/collapsed-frame-ui.tsx b/packages/render/src/collapsed-frame-ui.tsx index 1360497a0..1beaa42fa 100644 --- a/packages/render/src/collapsed-frame-ui.tsx +++ b/packages/render/src/collapsed-frame-ui.tsx @@ -3,6 +3,7 @@ import React, { useState } from "react"; import type { Frame } from "frames.js"; import type { FrameTheme, FrameState } from "./types"; import type { UseFrameReturnValue } from "./unstable-types"; +import { isValidPartialFrame } from "./ui/utils"; const defaultTheme: Required = { buttonBg: "#fff", @@ -21,13 +22,19 @@ const getThemeWithDefaults = (theme: FrameTheme): FrameTheme => { }; export type CollapsedFrameUIProps = { - frameState: FrameState | UseFrameReturnValue; + frameState: + | FrameState + | UseFrameReturnValue; theme?: FrameTheme; FrameImage?: React.FC & { src: string }>; allowPartialFrame?: boolean; }; -/** A UI component only, that should be easy for any app to integrate */ +/** + * A UI component only, that should be easy for any app to integrate. + * + * This component doesn't support Frames v2. + */ export function CollapsedFrameUI({ frameState, theme, @@ -50,12 +57,7 @@ export function CollapsedFrameUI({ if ( currentFrame.status === "done" && currentFrame.frameResult.status === "failure" && - !( - allowPartialFrame && - // Need at least image and buttons to render a partial frame - currentFrame.frameResult.frame.image && - currentFrame.frameResult.frame.buttons - ) + !(allowPartialFrame && isValidPartialFrame(currentFrame.frameResult)) ) { return null; } @@ -63,6 +65,11 @@ export function CollapsedFrameUI({ let frame: Frame | Partial | undefined; if (currentFrame.status === "done") { + if (currentFrame.frameResult.specification === "farcaster_v2") { + // Do not render farcaster frames v2 as collapsed because they don't have such UI + return null; + } + frame = currentFrame.frameResult.frame; } else if ( currentFrame.status === "message" || diff --git a/packages/render/src/frame-ui.tsx b/packages/render/src/frame-ui.tsx index b13e09d44..bc793c1ba 100644 --- a/packages/render/src/frame-ui.tsx +++ b/packages/render/src/frame-ui.tsx @@ -7,6 +7,7 @@ import type { FrameStackMessage, FrameStackRequestError, } from "./types"; +import { isValidPartialFrame } from "./ui/utils"; export const defaultTheme: Required = { buttonBg: "#fff", @@ -117,7 +118,13 @@ export type FrameUIProps = { enableImageDebugging?: boolean; }; -/** A UI component only, that should be easy for any app to integrate */ +/** + * A UI component only, that should be easy for any app to integrate. + * + * This component doesn't support Frames V2. + * + * @deprecated - please use `FrameUI` from `@frames.js/render/ui` instead. + */ export function FrameUI({ frameState, theme, @@ -143,12 +150,7 @@ export function FrameUI({ if ( currentFrame.status === "done" && currentFrame.frameResult.status === "failure" && - !( - allowPartialFrame && - // Need at least image and buttons to render a partial frame - currentFrame.frameResult.frame.image && - currentFrame.frameResult.frame.buttons - ) + !(allowPartialFrame && isValidPartialFrame(currentFrame.frameResult)) ) { return ; } @@ -157,6 +159,14 @@ export function FrameUI({ let debugImage: string | undefined; if (currentFrame.status === "done") { + // we don't support frames v2 in this component as it is deprecated + if ( + currentFrame.frameResult.specification !== "farcaster" && + currentFrame.frameResult.specification !== "openframes" + ) { + return null; + } + frame = currentFrame.frameResult.frame; debugImage = enableImageDebugging ? currentFrame.frameResult.framesDebugInfo?.image diff --git a/packages/render/src/next/validators.ts b/packages/render/src/next/validators.ts index a395f454e..6c03ce5c3 100644 --- a/packages/render/src/next/validators.ts +++ b/packages/render/src/next/validators.ts @@ -5,6 +5,6 @@ export function isSpecificationValid( ): specification is SupportedParsingSpecification { return ( typeof specification === "string" && - ["farcaster", "openframes"].includes(specification) + ["farcaster", "farcaster_v2", "openframes"].includes(specification) ); } diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index 6a524d703..03ffefe03 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -260,7 +260,7 @@ export type UseFrameOptions< * * @defaultValue 'farcaster' */ - specification?: SupportedParsingSpecification; + specification?: Exclude; /** * This function can be used to customize how error is reported to the user. */ @@ -539,7 +539,7 @@ export type FrameReducerActions = action: "RESET_INITIAL_FRAME"; resultOrFrame: ParseResult | Frame; homeframeUrl: string | null | undefined; - specification: SupportedParsingSpecification; + specification: Exclude; }; export type ButtonPressFunction< diff --git a/packages/render/src/ui/frame.base.tsx b/packages/render/src/ui/frame.base.tsx index 91e1fd0ec..a50edd7bf 100644 --- a/packages/render/src/ui/frame.base.tsx +++ b/packages/render/src/ui/frame.base.tsx @@ -1,4 +1,4 @@ -import type { Frame } from "frames.js"; +import type { Frame, FrameV2 } from "frames.js"; import { createElement as reactCreateElement, useCallback, @@ -8,6 +8,7 @@ import { } from "react"; import type { FrameState } from "../types"; import type { UseFrameReturnValue } from "../unstable-types"; +import { useFreshRef } from "../hooks/use-fresh-ref"; import type { FrameMessage, FrameUIComponents as BaseFrameUIComponents, @@ -15,6 +16,8 @@ import type { FrameUIState, RootContainerDimensions, RootContainerElement, + FrameButtonProps, + PartialFrameV2, } from "./types"; import { getErrorMessageFromFramesStackItem, @@ -27,6 +30,18 @@ export type FrameUIComponents> = export type FrameUITheme> = Partial>; +export type AppLaunchButtonPressEvent = + | { + status: "complete"; + frame: FrameV2; + frameUIState: FrameUIState; + } + | { + status: "partial"; + frame: PartialFrameV2; + frameUIState: FrameUIState; + }; + export type BaseFrameUIProps> = { frameState: | FrameState @@ -61,26 +76,51 @@ export type BaseFrameUIProps> = { * @defaultValue React.createElement */ createElement?: typeof reactCreateElement; + /** + * Called when user presses launch button on v2 frame. + * + * Only Frames v2 support this feature. + */ + onAppLaunchButtonPress?: (event: AppLaunchButtonPressEvent) => void; + /** + * Called when an error occurs in onAppLaunchButtonPress + * + * @defaultValue console.error() + */ + onAppLaunchButtonPressError?: (error: Error) => void; }; // eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop function defaultMessageHandler(): void {} +// eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop +function defaultOnAppLaunchButtonPress(): void {} + +function defaultErrorLogger(error: Error): void { + // eslint-disable-next-line no-console -- provide at least some feedback to the user + console.error(error); +} + export function BaseFrameUI>({ frameState, components, theme, allowPartialFrame = false, enableImageDebugging = false, - // eslint-disable-next-line no-console -- provide at least some feedback to the user - onError = console.error, + onError = defaultErrorLogger, + onAppLaunchButtonPressError = defaultErrorLogger, onMessage = defaultMessageHandler, createElement = reactCreateElement, + onAppLaunchButtonPress = defaultOnAppLaunchButtonPress, }: BaseFrameUIProps): JSX.Element | null { const [isImageLoading, setIsImageLoading] = useState(true); const { currentFrameStackItem } = frameState; const rootRef = useRef(null); const rootDimensionsRef = useRef(); + const onErrorRef = useFreshRef(onError); + const onAppLaunchButtonPressErrorRef = useFreshRef( + onAppLaunchButtonPressError + ); const onImageLoadEnd = useCallback(() => { setIsImageLoading(false); @@ -93,17 +133,13 @@ export function BaseFrameUI>({ } }, [currentFrameStackItem?.status]); - const onErrorRef = useRef(onError); - onErrorRef.current = onError; - useEffect(() => { if (currentFrameStackItem?.status === "requestError") { onErrorRef.current(currentFrameStackItem.requestError); } - }, [currentFrameStackItem]); + }, [currentFrameStackItem, onErrorRef]); - const onMessageRef = useRef(onMessage); - onMessageRef.current = onMessage; + const onMessageRef = useFreshRef(onMessage); useEffect(() => { if (currentFrameStackItem?.status === "message") { @@ -112,7 +148,7 @@ export function BaseFrameUI>({ status: currentFrameStackItem.type === "info" ? "message" : "error", }); } - }, [currentFrameStackItem]); + }, [currentFrameStackItem, onMessageRef]); if (!frameState.homeframeUrl) { return components.Error( @@ -128,8 +164,12 @@ export function BaseFrameUI>({ let frameUiState: FrameUIState; const previousFrameStackItem = frameState.framesStack[frameState.framesStack.length - 1]; + /** + * Frames v2 don' have previous frame as they consist purely of initial frame only + */ const previousFrame = - previousFrameStackItem?.status === "done" + previousFrameStackItem?.status === "done" && + previousFrameStackItem.frameResult.specification !== "farcaster_v2" ? previousFrameStackItem.frameResult.frame : null; @@ -201,30 +241,62 @@ export function BaseFrameUI>({ } case "done": { if (currentFrameStackItem.frameResult.status === "success") { - frameUiState = { - status: "complete", - frame: currentFrameStackItem.frameResult.frame, - debugImage: enableImageDebugging - ? currentFrameStackItem.frameResult.framesDebugInfo?.image - : undefined, - isImageLoading, - id: currentFrameStackItem.id, - frameState, - }; + if ( + currentFrameStackItem.frameResult.specification === "farcaster_v2" + ) { + frameUiState = { + status: "complete", + frame: currentFrameStackItem.frameResult.frame, + specification: "farcaster_v2", + debugImage: enableImageDebugging + ? currentFrameStackItem.frameResult.framesDebugInfo?.image + : undefined, + isImageLoading, + id: currentFrameStackItem.id, + frameState, + }; + } else { + frameUiState = { + status: "complete", + frame: currentFrameStackItem.frameResult.frame, + debugImage: enableImageDebugging + ? currentFrameStackItem.frameResult.framesDebugInfo?.image + : undefined, + isImageLoading, + id: currentFrameStackItem.id, + frameState, + }; + } } else if ( isPartialFrameStackItem(currentFrameStackItem) && allowPartialFrame ) { - frameUiState = { - status: "partial", - frame: currentFrameStackItem.frameResult.frame, - debugImage: enableImageDebugging - ? currentFrameStackItem.frameResult.framesDebugInfo?.image - : undefined, - isImageLoading, - id: currentFrameStackItem.id, - frameState, - }; + if ( + currentFrameStackItem.frameResult.specification === "farcaster_v2" + ) { + frameUiState = { + status: "partial", + frame: currentFrameStackItem.frameResult.frame, + specification: "farcaster_v2", + debugImage: enableImageDebugging + ? currentFrameStackItem.frameResult.framesDebugInfo?.image + : undefined, + isImageLoading, + id: currentFrameStackItem.id, + frameState, + }; + } else { + frameUiState = { + status: "partial", + frame: currentFrameStackItem.frameResult.frame, + debugImage: enableImageDebugging + ? currentFrameStackItem.frameResult.framesDebugInfo?.image + : undefined, + isImageLoading, + id: currentFrameStackItem.id, + frameState, + }; + } } else { return components.Error( { message: "Invalid frame" }, @@ -247,33 +319,70 @@ export function BaseFrameUI>({ const isLoading = frameUiState.status === "loading" || frameUiState.isImageLoading; - const buttonsProps = - frameUiState.status === "loading" || - !frameUiState.frame.buttons || - frameUiState.frame.buttons.length === 0 - ? null - : frameUiState.frame.buttons.map((frameButton, index) => ({ + let buttonsProps: FrameButtonProps[] | null = null; + + if (frameUiState.status !== "loading") { + if ("specification" in frameUiState) { + buttonsProps = [ + { frameState: frameUiState, - frameButton, - index, + frameButton: { + action: "launch", + label: frameUiState.frame.button.title, + }, + index: 0, isDisabled: false, onPress() { - // track dimensions of the root if possible - rootDimensionsRef.current = rootRef.current?.computeDimensions(); - - Promise.resolve( - frameState.onButtonPress( - // @todo change the type onButtonPress to accept partial frame as well because that can happen if partial frames are enabled - frameUiState.frame as Frame, - frameButton, - index - ) - ).catch((error) => { - // eslint-disable-next-line no-console -- provide feedback to the user - console.error(error); - }); + // we don't need to track dimensions here because this button does nothing to frame stack + try { + onAppLaunchButtonPress( + frameUiState.status === "complete" + ? { + status: "complete", + frame: frameUiState.frame, + frameUIState: frameUiState, + } + : { + status: "partial", + frame: frameUiState.frame, + frameUIState: frameUiState, + } + ); + } catch (e) { + onAppLaunchButtonPressErrorRef.current( + e instanceof Error ? e : new Error(String(e)) + ); + } }, - })); + }, + ]; + } else if ( + frameUiState.frame.buttons && + frameUiState.frame.buttons.length > 0 + ) { + buttonsProps = frameUiState.frame.buttons.map((frameButton, index) => ({ + frameState: frameUiState, + frameButton, + index, + isDisabled: false, + onPress() { + // track dimensions of the root if possible + rootDimensionsRef.current = rootRef.current?.computeDimensions(); + + Promise.resolve( + frameState.onButtonPress( + // @todo change the type onButtonPress to accept partial frame as well because that can happen if partial frames are enabled + frameUiState.frame as Frame, + frameButton, + index + ) + ).catch((e) => { + onErrorRef.current(e instanceof Error ? e : new Error(String(e))); + }); + }, + })); + } + } return components.Root( { @@ -309,9 +418,12 @@ export function BaseFrameUI>({ { frameState: frameUiState, aspectRatio: + // eslint-disable-next-line no-nested-ternary -- unnecessary to extract this to a variable (frameUiState.status === "loading" ? previousFrame?.imageAspectRatio - : frameUiState.frame.imageAspectRatio) ?? "1.91:1", + : "specification" in frameUiState + ? "1.91:1" + : frameUiState.frame.imageAspectRatio) ?? "1.91:1", image: components.Image( frameUiState.status === "loading" ? { @@ -323,8 +435,15 @@ export function BaseFrameUI>({ : { status: "frame-loading-complete", frameState: frameUiState, - src: frameUiState.debugImage ?? frameUiState.frame.image, - aspectRatio: frameUiState.frame.imageAspectRatio ?? "1.91:1", + src: + frameUiState.debugImage ?? + ("specification" in frameUiState + ? frameUiState.frame.imageUrl + : frameUiState.frame.image), + aspectRatio: + ("specification" in frameUiState + ? undefined + : frameUiState.frame.imageAspectRatio) ?? "1.91:1", onImageLoadEnd, }, theme?.Image || ({} as TStylingProps) @@ -350,6 +469,7 @@ export function BaseFrameUI>({ ), textInputContainer: frameUiState.status === "loading" || + "specification" in frameUiState || typeof frameUiState.frame.inputText !== "string" ? null : components.TextInputContainer( diff --git a/packages/render/src/ui/types.ts b/packages/render/src/ui/types.ts index b4ed6562b..56934dfd3 100644 --- a/packages/render/src/ui/types.ts +++ b/packages/render/src/ui/types.ts @@ -1,5 +1,6 @@ -import type { Frame, FrameButton } from "frames.js"; +import type { Frame, FrameButton, FrameV2 } from "frames.js"; import type { createElement, ReactElement } from "react"; +import type { ParsedFrameV2 } from "frames.js/frame-parsers"; import type { FrameState } from "../types"; import type { UseFrameReturnValue } from "../unstable-types"; @@ -30,28 +31,82 @@ type RequiredFrameProperties = "image" | "buttons"; export type PartialFrame = Omit, RequiredFrameProperties> & Required>; -export type FrameUIState = - | { - status: "loading"; - id: number; - frameState: FrameState | UseFrameReturnValue; - } - | { - id: number; - status: "partial"; - frame: PartialFrame; - frameState: FrameState | UseFrameReturnValue; - debugImage?: string; - isImageLoading: boolean; - } - | { - id: number; - status: "complete"; - frame: Frame; - frameState: FrameState | UseFrameReturnValue; - debugImage?: string; - isImageLoading: boolean; +/** + * If partial frame rendering is enabled this is the shape of the frame + */ +export type PartialFrameV2 = Omit & { + imageUrl: NonNullable; + button: Omit, "action" | "title"> & { + action: Omit< + NonNullable["action"]>, + "url" | "title" + > & { + url: NonNullable< + NonNullable["action"]>["url"] + >; }; + title: NonNullable< + NonNullable["title"]> + >; + }; +}; + +type FrameUIStateLoading = { + status: "loading"; + id: number; + frameState: FrameState | UseFrameReturnValue; +}; + +/** + * Frame is partial. Available only if allowPartialFrame prop is enabled. + */ +export type FrameUIStatePartialFramesV1 = { + id: number; + status: "partial"; + frame: PartialFrame; + frameState: FrameState | UseFrameReturnValue; + debugImage?: string; + isImageLoading: boolean; +}; + +/** + * Frame is partial. Available only if allowPartialFrame prop is enabled. + */ +export type FrameUIStatePartialFramesV2 = { + id: number; + status: "partial"; + frame: PartialFrameV2; + specification: "farcaster_v2"; + frameState: FrameState | UseFrameReturnValue; + debugImage?: string; + isImageLoading: boolean; +}; + +export type FrameUIStateCompleteFramesV1 = { + id: number; + status: "complete"; + frame: Frame; + frameState: FrameState | UseFrameReturnValue; + debugImage?: string; + isImageLoading: boolean; +}; + +export type FrameUIStateCompleteFramesV2 = { + id: number; + status: "complete"; + specification: "farcaster_v2"; + frame: FrameV2; + frameState: FrameState | UseFrameReturnValue; + debugImage?: string; + isImageLoading: boolean; +}; + +export type FrameUIState = + | FrameUIStateLoading + | FrameUIStatePartialFramesV1 + | FrameUIStatePartialFramesV2 + | FrameUIStateCompleteFramesV1 + | FrameUIStateCompleteFramesV2; type FrameUIStateProps = { frameState: FrameUIState; @@ -242,9 +297,14 @@ export type FrameImageProps = FrameUIStateProps & { } ); +export type FrameLaunchButton = { + action: "launch"; + label: string; +}; + export type FrameButtonProps = { isDisabled: boolean; - frameButton: FrameButton; + frameButton: FrameButton | FrameLaunchButton; onPress: () => void; index: number; } & FrameUIStateProps; diff --git a/packages/render/src/ui/utils.ts b/packages/render/src/ui/utils.ts index 4abf00680..9b5eeb0af 100644 --- a/packages/render/src/ui/utils.ts +++ b/packages/render/src/ui/utils.ts @@ -10,14 +10,30 @@ import type { FrameStackMessage as UnstableFrameStackMessage, FrameStackRequestError as UnstableFrameStackRequestError, } from "../unstable-types"; -import type { PartialFrame } from "./types"; +import type { PartialFrame, PartialFrameV2 } from "./types"; type FrameResultFailure = Exclude; +type FrameResultFailureFrameV1 = Extract< + FrameResultFailure, + { specification: "farcaster" | "openframes" } +>; + +type FrameV1FailureResult = Omit & { + frame: PartialFrame; +}; + +type FrameResultFailureFrameV2 = Extract< + FrameResultFailure, + { specification: "farcaster_v2" } +>; + +type FrameV2FailureResult = Omit & { + frame: PartialFrameV2; +}; + type FrameStackItemWithPartialFrame = Omit & { - frameResult: Omit & { - frame: PartialFrame; - }; + frameResult: FrameV1FailureResult | FrameV2FailureResult; }; export function isPartialFrameStackItem( @@ -26,9 +42,8 @@ export function isPartialFrameStackItem( return ( stackItem.status === "done" && stackItem.frameResult.status === "failure" && - !!stackItem.frameResult.frame.image && - !!stackItem.frameResult.frame.buttons && - stackItem.frameResult.frame.buttons.length > 0 + (isValidPartialFrameV1(stackItem.frameResult) || + isValidPartialFrameV2(stackItem.frameResult)) ); } @@ -49,3 +64,45 @@ export function getErrorMessageFromFramesStackItem( return "An error occurred"; } + +export function isValidPartialFrameV1( + value: GetFrameResult +): value is FrameV1FailureResult { + if ( + value.specification !== "farcaster" && + value.specification !== "openframes" + ) { + return false; + } + + return ( + !!value.frame.image && + !!value.frame.buttons && + value.frame.buttons.length > 0 + ); +} + +export function isValidPartialFrameV2( + value: GetFrameResult +): value is FrameV2FailureResult { + if (value.specification !== "farcaster_v2") { + return false; + } + + return ( + !!value.frame.imageUrl && + !!value.frame.button && + !!value.frame.button.title && + !!value.frame.button.action && + !!value.frame.button.action.url + ); +} + +/** + * All partial frames need at least an image and button to be considered valid. + */ +export function isValidPartialFrame(frameResult: GetFrameResult): boolean { + return ( + isValidPartialFrameV1(frameResult) || isValidPartialFrameV2(frameResult) + ); +} diff --git a/packages/render/src/use-frame-stack.ts b/packages/render/src/use-frame-stack.ts index 8dda97991..0dda2e8c4 100644 --- a/packages/render/src/use-frame-stack.ts +++ b/packages/render/src/use-frame-stack.ts @@ -118,7 +118,7 @@ function framesStackReducer( type UseFrameStackOptions = { initialFrame?: Frame | ParseResult; initialFrameUrl?: string | null; - initialSpecification: SupportedParsingSpecification; + initialSpecification: Exclude; }; export type FrameStackAPI = { From e2960ff2ae0edd9c5c0710358c93420d5a074579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:28:49 +0100 Subject: [PATCH 06/88] fix: properly type current frame stack item --- packages/render/src/unstable-types.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index 8433fed8f..a48741b7f 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -246,7 +246,15 @@ export type UseFrameReturnValue< > >; /** The frame at the top of the stack (at index 0) */ - readonly currentFrameStackItem: FramesStackItem | undefined; + readonly currentFrameStackItem: + | FramesStackItem< + TExtraDataPending, + TExtraDataDone, + TExtraDataDoneRedirect, + TExtraDataRequestError, + TExtraDataMesssage + > + | undefined; /** A stack of frames with additional context, with the most recent frame at index 0 */ readonly framesStack: FramesStack< TExtraDataPending, From bc429a19f8e5459f72d8945803892e86d807c997 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:31:31 +0100 Subject: [PATCH 07/88] refactor: types --- packages/render/src/unstable-types.ts | 138 +++++++++++++++++++------- 1 file changed, 101 insertions(+), 37 deletions(-) diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index a48741b7f..de949ecd7 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -40,6 +40,18 @@ export type ResolvedSigner = { frameContext?: FrameContext; }; +export type OnTransactionFunction = OnTransactionFunc; + +export type OnSignatureFunction = OnSignatureFunc; + +export type OnMintFunction = (t: OnMintArgs) => void; + +export type OnErrorFunction = (error: Error) => void; + +export type OnLinkButtonClickFunction = (button: FrameButtonLink) => void; + +export type OnRedirectFunction = (location: URL) => void; + export type ResolveSignerFunctionArg = { parseResult: ParseFramesWithReportsResult; }; @@ -112,13 +124,13 @@ export type UseFrameOptions< */ resolveAddress: ResolveAddressFunction; /** a function to handle mint buttons */ - onMint?: (t: OnMintArgs) => void; + onMint?: OnMintFunction; /** a function to handle transaction buttons that returned transaction data from the target, returns the transaction hash or null */ - onTransaction?: OnTransactionFunc; + onTransaction?: OnTransactionFunction; /** Transaction data suffix */ transactionDataSuffix?: `0x${string}`; /** A function to handle transaction buttons that returned signature data from the target, returns signature hash or null */ - onSignature?: OnSignatureFunc; + onSignature?: OnSignatureFunction; /** * Extra data appended to the frame action payload */ @@ -126,11 +138,11 @@ export type UseFrameOptions< /** * This function can be used to customize how error is reported to the user. */ - onError?: (error: Error) => void; + onError?: OnErrorFunction; /** * This function can be used to customize how the link button click is handled. */ - onLinkButtonClick?: (button: FrameButtonLink) => void; + onLinkButtonClick?: OnLinkButtonClickFunction; } & Partial< Pick< UseFetchFrameOptions, @@ -330,6 +342,73 @@ export type FrameReducerActions< extra: TExtraDone; }; +export type OnTransactionDataStartEvent = { + button: FrameButtonTx; +}; + +export type OnTransactionDataStartFunction = ( + event: OnTransactionDataStartEvent +) => void; + +type OnTransactionDataSuccessEvent = { + button: FrameButtonTx; + data: TransactionTargetResponse; +}; + +export type OnTransactionDataSuccessFunction = ( + event: OnTransactionDataSuccessEvent +) => void; + +export type OnTransactionStartEvent = { + button: FrameButtonTx; + data: TransactionTargetResponseSendTransaction; +}; + +export type OnTransactionStartFunction = ( + event: OnTransactionStartEvent +) => void; + +export type OnTransactionSuccessEvent = { + button: FrameButtonTx; +}; + +export type OnTransactionSuccessFunction = ( + event: OnTransactionSuccessEvent +) => void; + +export type OnSignatureStartEvent = { + button: FrameButtonTx; + data: TransactionTargetResponseSignTypedDataV4; +}; + +export type OnSignatureStartFunction = (event: OnSignatureStartEvent) => void; + +export type OnSignatureSuccessEvent = { + button: FrameButtonTx; +}; + +export type OnSignatureSuccessFunction = ( + event: OnSignatureSuccessEvent +) => void; + +type OnTransactionProcessingStartEvent = { + button: FrameButtonTx; + transactionId: `0x${string}`; +}; + +export type OnTransactionProcessingStartFunction = ( + event: OnTransactionProcessingStartEvent +) => void; + +type OnTransactionProcessingSuccessEvent = { + button: FrameButtonTx; + transactionId: `0x${string}`; +}; + +export type OnTransactionProcessingSuccessFunction = ( + event: OnTransactionProcessingSuccessEvent +) => void; + export type UseFetchFrameOptions< TExtraPending = unknown, TExtraDone = unknown, @@ -360,16 +439,16 @@ export type UseFetchFrameOptions< /** * Called after transaction data has been returned from the server and user needs to approve the transaction. */ - onTransaction: OnTransactionFunc; + onTransaction: OnTransactionFunction; /** Transaction data suffix */ transactionDataSuffix?: `0x${string}`; - onSignature: OnSignatureFunc; + onSignature: OnSignatureFunction; /** * This function can be used to customize how error is reported to the user. * * Should be memoized */ - onError?: (error: Error) => void; + onError?: OnErrorFunction; /** * Custom fetch compatible function used to make requests. * @@ -379,73 +458,58 @@ export type UseFetchFrameOptions< /** * This function is called when the frame returns a redirect in response to post_redirect button click. */ - onRedirect: (location: URL) => void; + onRedirect: OnRedirectFunction; /** * Called when user presses the tx button just before the action is signed and sent to the server * to obtain the transaction data. */ - onTransactionDataStart?: (event: { button: FrameButtonTx }) => void; + onTransactionDataStart?: OnTransactionDataStartFunction; /** * Called when transaction data has been successfully returned from the server. */ - onTransactionDataSuccess?: (event: { - button: FrameButtonTx; - data: TransactionTargetResponse; - }) => void; + onTransactionDataSuccess?: OnTransactionDataSuccessFunction; /** * Called when anything failed between onTransactionDataStart and obtaining the transaction data. */ - onTransactionDataError?: (error: Error) => void; + onTransactionDataError?: OnErrorFunction; /** * Called before onTransaction() is called * Called after onTransactionDataSuccess() is called */ - onTransactionStart?: (event: { - button: FrameButtonTx; - data: TransactionTargetResponseSendTransaction; - }) => void; + onTransactionStart?: OnTransactionStartFunction; /** * Called when onTransaction() returns a transaction id */ - onTransactionSuccess?: (event: { button: FrameButtonTx }) => void; + onTransactionSuccess?: OnTransactionSuccessFunction; /** * Called when onTransaction() fails to return a transaction id */ - onTransactionError?: (error: Error) => void; + onTransactionError?: OnErrorFunction; /** * Called before onSignature() is called * Called after onTransactionDataSuccess() is called */ - onSignatureStart?: (event: { - button: FrameButtonTx; - data: TransactionTargetResponseSignTypedDataV4; - }) => void; + onSignatureStart?: OnSignatureStartFunction; /** * Called when onSignature() returns a transaction id */ - onSignatureSuccess?: (event: { button: FrameButtonTx }) => void; + onSignatureSuccess?: OnSignatureSuccessFunction; /** * Called when onSignature() fails to return a transaction id */ - onSignatureError?: (error: Error) => void; + onSignatureError?: OnErrorFunction; /** * Called after either onSignatureSuccess() or onTransactionSuccess() is called just before the transaction is sent to the server. */ - onTransactionProcessingStart?: (event: { - button: FrameButtonTx; - transactionId: `0x${string}`; - }) => void; + onTransactionProcessingStart?: OnTransactionProcessingStartFunction; /** * Called after the transaction has been successfully sent to the server and returned a success response. */ - onTransactionProcessingSuccess?: (event: { - button: FrameButtonTx; - transactionId: `0x${string}`; - }) => void; + onTransactionProcessingSuccess?: OnTransactionProcessingSuccessFunction; /** * Called when the transaction has been sent to the server but the server returned an error. */ - onTransactionProcessingError?: (error: Error) => void; + onTransactionProcessingError?: OnErrorFunction; }; export type FetchFrameFunction = ( @@ -667,7 +731,7 @@ export type SignerComposerActionResult = { messageHash: `0x${string}`; timestamp: number; network: number; - buttonIndex: 1; + buttonIndex: number; state: string; }; trustedData: { From 1ef438770c0605fb6a1e102e0c48e89f2c71f387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:33:53 +0100 Subject: [PATCH 08/88] feat: useCastAction hook and fixing issues with signer copy --- packages/render/src/farcaster/frames.tsx | 83 ++- packages/render/src/farcaster/signers.tsx | 8 +- packages/render/src/helpers.ts | 32 +- .../farcaster/use-farcaster-identity.tsx | 81 ++- .../use-farcaster-multi-identity.tsx | 88 ++- .../src/identity/lens/use-lens-identity.tsx | 197 ++++--- .../src/identity/xmtp/use-xmtp-identity.tsx | 92 ++- packages/render/src/unstable-types.ts | 39 +- packages/render/src/use-cast-action.ts | 543 ++++++++++++++++++ 9 files changed, 1010 insertions(+), 153 deletions(-) create mode 100644 packages/render/src/use-cast-action.ts diff --git a/packages/render/src/farcaster/frames.tsx b/packages/render/src/farcaster/frames.tsx index 64f3ec037..895f9fa14 100644 --- a/packages/render/src/farcaster/frames.tsx +++ b/packages/render/src/farcaster/frames.tsx @@ -16,7 +16,9 @@ import type { SignFrameActionFunc, } from "../types"; import type { - SignComposerActionFunc, + SignCastActionFunction, + SignComposerActionFunction, + SignerStateCastActionContext, SignerStateComposerActionContext, } from "../unstable-types"; import { tryCallAsync } from "../helpers"; @@ -26,7 +28,7 @@ import type { FarcasterFrameContext } from "./types"; /** * Creates a singer request payload to fetch composer action url. */ -export const signComposerAction: SignComposerActionFunc = +export const signComposerAction: SignComposerActionFunction = async function signComposerAction(signerPrivateKey, actionContext) { const messageOrError = await tryCallAsync(() => createComposerActionMessageWithSignerKey(signerPrivateKey, actionContext) @@ -40,13 +42,40 @@ export const signComposerAction: SignComposerActionFunc = return { untrustedData: { - buttonIndex: 1, - fid: actionContext.fid, + buttonIndex: message.data.frameActionBody.buttonIndex, + fid: message.data.fid, messageHash: bytesToHex(message.hash), - network: 1, + network: FarcasterNetwork.MAINNET, state: Buffer.from(message.data.frameActionBody.state).toString(), - timestamp: new Date().getTime(), - url: actionContext.url, + timestamp: message.data.timestamp, + url: Buffer.from(message.data.frameActionBody.url).toString(), + }, + trustedData: { + messageBytes: trustedBytes, + }, + }; + }; + +export const signCastAction: SignCastActionFunction = + async function signCastAction(signerPrivateKey, actionContext) { + const messageOrError = await tryCallAsync(() => + createCastActionMessageWithSignerKey(signerPrivateKey, actionContext) + ); + + if (messageOrError instanceof Error) { + throw messageOrError; + } + + const { message, trustedBytes } = messageOrError; + + return { + untrustedData: { + buttonIndex: message.data.frameActionBody.buttonIndex, + fid: message.data.fid, + messageHash: bytesToHex(message.hash), + network: FarcasterNetwork.MAINNET, + timestamp: message.data.timestamp, + url: Buffer.from(message.data.frameActionBody.url).toString(), }, trustedData: { messageBytes: trustedBytes, @@ -177,6 +206,46 @@ export async function createComposerActionMessageWithSignerKey( return { message: messageData, trustedBytes }; } +export async function createCastActionMessageWithSignerKey( + signerKey: string, + { fid, castId, postUrl }: SignerStateCastActionContext +): Promise<{ + message: FrameActionMessage; + trustedBytes: string; +}> { + const signer = new NobleEd25519Signer(Buffer.from(signerKey.slice(2), "hex")); + + const messageDataOptions = { + fid, + network: FarcasterNetwork.MAINNET, + }; + + const message = await makeFrameAction( + FrameActionBody.create({ + url: Buffer.from(postUrl), + buttonIndex: 1, + castId: { + fid: castId.fid, + hash: hexToBytes(castId.hash), + }, + }), + messageDataOptions, + signer + ); + + if (message.isErr()) { + throw message.error; + } + + const messageData = message.value; + + const trustedBytes = Buffer.from( + Message.encode(message._unsafeUnwrap()).finish() + ).toString("hex"); + + return { message: messageData, trustedBytes }; +} + export async function createFrameActionMessageWithSignerKey( signerKey: string, { diff --git a/packages/render/src/farcaster/signers.tsx b/packages/render/src/farcaster/signers.tsx index 58fe6b2a6..14cb6b167 100644 --- a/packages/render/src/farcaster/signers.tsx +++ b/packages/render/src/farcaster/signers.tsx @@ -1,5 +1,8 @@ import type { FrameActionBodyPayload, SignerStateInstance } from "../types"; -import type { SignComposerActionFunc } from "../unstable-types"; +import type { + SignCastActionFunction, + SignComposerActionFunction, +} from "../unstable-types"; import type { FarcasterFrameContext } from "./types"; export type FarcasterSignerState = @@ -8,7 +11,8 @@ export type FarcasterSignerState = FrameActionBodyPayload, FarcasterFrameContext > & { - signComposerAction: SignComposerActionFunc; + signComposerAction: SignComposerActionFunction; + signCastAction: SignCastActionFunction; }; export type FarcasterSignerPendingApproval = { diff --git a/packages/render/src/helpers.ts b/packages/render/src/helpers.ts index c18382b23..cb6900680 100644 --- a/packages/render/src/helpers.ts +++ b/packages/render/src/helpers.ts @@ -2,7 +2,11 @@ import type { ParseFramesWithReportsResult, ParseResult, } from "frames.js/frame-parsers"; -import type { ComposerActionFormResponse } from "frames.js/types"; +import type { + CastActionFrameResponse, + CastActionMessageResponse, + ComposerActionFormResponse, +} from "frames.js/types"; import type { PartialFrame } from "./ui/types"; export async function tryCallAsync( @@ -84,6 +88,32 @@ export function isComposerFormActionResponse( ); } +export function isCastActionFrameResponse( + response: unknown +): response is CastActionFrameResponse { + return ( + typeof response === "object" && + response !== null && + "type" in response && + response.type === "frame" && + "frameUrl" in response && + typeof response.frameUrl === "string" + ); +} + +export function isCastActionMessageResponse( + response: unknown +): response is CastActionMessageResponse { + return ( + typeof response === "object" && + response !== null && + "type" in response && + response.type === "message" && + "message" in response && + typeof response.message === "string" + ); +} + /** * Merges all search params in order from left to right into the URL. * diff --git a/packages/render/src/identity/farcaster/use-farcaster-identity.tsx b/packages/render/src/identity/farcaster/use-farcaster-identity.tsx index e8d7c27e2..691c8fbc6 100644 --- a/packages/render/src/identity/farcaster/use-farcaster-identity.tsx +++ b/packages/render/src/identity/farcaster/use-farcaster-identity.tsx @@ -8,7 +8,11 @@ import { } from "react"; import { convertKeypairToHex, createKeypairEDDSA } from "../crypto"; import type { FarcasterSignerState } from "../../farcaster"; -import { signComposerAction, signFrameAction } from "../../farcaster"; +import { + signComposerAction, + signCastAction, + signFrameAction, +} from "../../farcaster"; import type { Storage } from "../types"; import { useVisibilityDetection } from "../../hooks/use-visibility-detection"; import { WebStorage } from "../storage"; @@ -205,6 +209,7 @@ export function useFarcasterIdentity({ const generateUserIdRef = useFreshRef(generateUserId); const onMissingIdentityRef = useFreshRef(onMissingIdentity); const fetchFnRef = useFreshRef(fetchFn); + const signerUrlRef = useFreshRef(signerUrl); const createFarcasterSigner = useCallback(async (): Promise => { @@ -213,7 +218,7 @@ export function useFarcasterIdentity({ const keypairString = convertKeypairToHex(keypair); const authorizationResponse = await fetchFnRef.current( // real signer or local one are handled by local route so we don't need to expose anything to client side bundle - signerUrl, + signerUrlRef.current, { method: "POST", body: JSON.stringify({ @@ -311,7 +316,13 @@ export function useFarcasterIdentity({ console.error("@frames.js/render: API Call failed", error); throw error; } - }, [fetchFnRef, generateUserIdRef, onLogInStartRef, setState, signerUrl]); + }, [ + fetchFnRef, + generateUserIdRef, + onLogInStartRef, + setState, + signerUrlRef, + ]); const impersonateUser = useCallback( async (fid: number) => { @@ -432,16 +443,43 @@ export function useFarcasterIdentity({ onLogInRef, ]); - return useMemo( - () => ({ + const farcasterUserRef = useFreshRef(farcasterUser); + const isLoadingRef = useFreshRef(isLoading); + + return useMemo(() => { + /** + * These are here only for backwards compatiblity so value is invalidate on change of these + * without the necessity to refactor the whole signer to have some sort of event handlers + * that will be able to react to changes (although that would be useful as well). + * + * We are using refs to fetch the current value in getters below so these are just to make eslint happy + * without the necessity to disable the check on hook dependencies. + * + * We have getters here because there is an edge case if you have identity hook with async storage + * and you resolve the signer in useFrame() before the signer internal values are resolved. + * That leads into an edge case when useFrame() behaves like you aren't signed in but you are because + * the return value of memo is copied. + */ + void farcasterUser; + void isLoading; + + return { specification: "farcaster", - signer: farcasterUser, - hasSigner: - farcasterUser?.status === "approved" || - farcasterUser?.status === "impersonating", + get signer() { + return farcasterUserRef.current; + }, + get hasSigner() { + return ( + farcasterUserRef.current?.status === "approved" || + farcasterUserRef.current?.status === "impersonating" + ); + }, signFrameAction, + signCastAction, signComposerAction, - isLoadingSigner: isLoading, + get isLoadingSigner() { + return isLoadingRef.current; + }, impersonateUser, onSignerlessFramePress, createSigner, @@ -453,15 +491,16 @@ export function useFarcasterIdentity({ frameContext, }; }, - }), - [ - farcasterUser, - identityPoller, - impersonateUser, - isLoading, - logout, - createSigner, - onSignerlessFramePress, - ] - ); + }; + }, [ + farcasterUser, + isLoading, + impersonateUser, + onSignerlessFramePress, + createSigner, + logout, + identityPoller, + farcasterUserRef, + isLoadingRef, + ]); } diff --git a/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx b/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx index f4e1eeb13..87b87aaae 100644 --- a/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx +++ b/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx @@ -8,7 +8,11 @@ import { } from "react"; import { convertKeypairToHex, createKeypairEDDSA } from "../crypto"; import type { FarcasterSignerState } from "../../farcaster"; -import { signComposerAction, signFrameAction } from "../../farcaster"; +import { + signComposerAction, + signCastAction, + signFrameAction, +} from "../../farcaster"; import type { Storage } from "../types"; import { useVisibilityDetection } from "../../hooks/use-visibility-detection"; import { WebStorage } from "../storage"; @@ -278,6 +282,7 @@ export function useFarcasterMultiIdentity({ const generateUserIdRef = useFreshRef(generateUserId); const onMissingIdentityRef = useFreshRef(onMissingIdentity); const fetchFnRef = useFreshRef(fetchFn); + const signerUrlRef = useFreshRef(signerUrl); const createFarcasterSigner = useCallback(async (): Promise => { @@ -286,7 +291,7 @@ export function useFarcasterMultiIdentity({ const keypairString = convertKeypairToHex(keypair); const authorizationResponse = await fetchFnRef.current( // real signer or local one are handled by local route so we don't need to expose anything to client side bundle - signerUrl, + signerUrlRef.current, { method: "POST", body: JSON.stringify({ @@ -396,7 +401,13 @@ export function useFarcasterMultiIdentity({ console.error("@frames.js/render: API Call failed", error); throw error; } - }, [fetchFnRef, generateUserIdRef, onLogInStartRef, setState, signerUrl]); + }, [ + fetchFnRef, + generateUserIdRef, + onLogInStartRef, + setState, + signerUrlRef, + ]); const impersonateUser = useCallback( async (fid: number) => { @@ -514,7 +525,7 @@ export function useFarcasterMultiIdentity({ unregisterVisibilityChangeListener(); }; } - }, [farcasterUser, identityPoller, visibilityDetector, setState]); + }, [farcasterUser, identityPoller, visibilityDetector, setState, onLogInRef]); const selectIdentity = useCallback( async (id: number | string) => { @@ -531,25 +542,46 @@ export function useFarcasterMultiIdentity({ return newState; }); }, - [setState] + [onIdentitySelectRef, setState] ); - return useMemo( - () => ({ + const farcasterUserRef = useFreshRef(farcasterUser); + const isLoadingRef = useFreshRef(isLoading); + const identitiesRef = useFreshRef(state.identities); + + return useMemo(() => { + /** + * See the explanation in useFarcasterIdentity() + */ + void farcasterUser; + void isLoading; + void state.identities; + + return { specification: "farcaster", - signer: farcasterUser, - hasSigner: - farcasterUser?.status === "approved" || - farcasterUser?.status === "impersonating", + get signer() { + return farcasterUserRef.current; + }, + get hasSigner() { + return ( + farcasterUserRef.current?.status === "approved" || + farcasterUserRef.current?.status === "impersonating" + ); + }, signFrameAction, + signCastAction, signComposerAction, - isLoadingSigner: isLoading, + get isLoadingSigner() { + return isLoadingRef.current; + }, impersonateUser, onSignerlessFramePress, createSigner, logout, removeIdentity, - identities: state.identities, + get identities() { + return identitiesRef.current; + }, selectIdentity, identityPoller, withContext(frameContext) { @@ -558,18 +590,20 @@ export function useFarcasterMultiIdentity({ frameContext, }; }, - }), - [ - farcasterUser, - identityPoller, - impersonateUser, - isLoading, - logout, - createSigner, - onSignerlessFramePress, - removeIdentity, - selectIdentity, - state.identities, - ] - ); + }; + }, [ + impersonateUser, + onSignerlessFramePress, + createSigner, + logout, + removeIdentity, + selectIdentity, + identityPoller, + farcasterUserRef, + farcasterUser, + isLoading, + isLoadingRef, + state.identities, + identitiesRef, + ]); } diff --git a/packages/render/src/identity/lens/use-lens-identity.tsx b/packages/render/src/identity/lens/use-lens-identity.tsx index 2efcf0703..8fe0a2642 100644 --- a/packages/render/src/identity/lens/use-lens-identity.tsx +++ b/packages/render/src/identity/lens/use-lens-identity.tsx @@ -1,7 +1,12 @@ import { useConnectModal } from "@rainbow-me/rainbowkit"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { useAccount, useConfig, useConnections } from "wagmi"; -import { signMessage, signTypedData, switchChain } from "wagmi/actions"; +import { + useAccount, + useConnections, + useSignTypedData, + useSignMessage, + useSwitchChain, +} from "wagmi"; import { LensClient, development, production } from "@lens-protocol/client"; import type { SignerStateActionContext, @@ -10,6 +15,7 @@ import type { } from "../../types"; import type { Storage } from "../types"; import { WebStorage } from "../storage"; +import { useFreshRef } from "../../hooks/use-fresh-ref"; import type { LensFrameContext } from "./use-lens-context"; export type LensProfile = { @@ -84,9 +90,14 @@ export function useLensIdentity({ const [showProfileSelector, setShowProfileSelector] = useState(false); const [availableProfiles, setAvailableProfiles] = useState([]); const connect = useConnectModal(); - const config = useConfig(); const { address } = useAccount(); const activeConnection = useConnections(); + const lensSignerRef = useFreshRef(lensSigner); + const { switchChainAsync } = useSwitchChain(); + const { signMessageAsync } = useSignMessage(); + const { signTypedDataAsync } = useSignTypedData(); + const storageKeyRef = useFreshRef(storageKey); + const addressRef = useFreshRef(address); const lensClient = useRef( new LensClient({ @@ -94,6 +105,8 @@ export function useLensIdentity({ }) ).current; + const connectRef = useFreshRef(connect); + useEffect(() => { storageRef.current .get(storageKey) @@ -109,22 +122,26 @@ export function useLensIdentity({ }, [storageKey]); const logout = useCallback(async () => { - await storageRef.current.delete(storageKey); + await storageRef.current.delete(storageKeyRef.current); setLensSigner(null); - }, [storageKey]); + }, [storageKeyRef]); const handleSelectProfile = useCallback( async (profile: LensProfile) => { try { - if (!address) { + const walletAddress = addressRef.current; + + if (!walletAddress) { throw new Error("No wallet connected"); } + setShowProfileSelector(false); + const { id, text } = await lensClient.authentication.generateChallenge({ - signedBy: address, + signedBy: walletAddress, for: profile.id, }); - const signature = await signMessage(config, { + const signature = await signMessageAsync({ message: { raw: typeof text === "string" @@ -132,7 +149,9 @@ export function useLensIdentity({ : Buffer.from(text as Uint8Array), }, }); + await lensClient.authentication.authenticate({ id, signature }); + const accessTokenResult = await lensClient.authentication.getAccessToken(); const identityTokenResult = @@ -151,12 +170,15 @@ export function useLensIdentity({ const signer: LensSigner = { accessToken, profileId, - address, + address: walletAddress, identityToken, handle, }; - await storageRef.current.set(storageKey, () => signer); + await storageRef.current.set( + storageKeyRef.current, + () => signer + ); setLensSigner(signer); } @@ -165,36 +187,48 @@ export function useLensIdentity({ console.error("@frames.js/render: Create Lens signer failed", error); } }, - [address, config, lensClient.authentication, lensClient.profile, storageKey] + [ + addressRef, + lensClient.authentication, + lensClient.profile, + signMessageAsync, + storageKeyRef, + ] ); const onSignerlessFramePress = useCallback(async () => { try { setIsLoading(true); - if (!lensSigner) { - if (!address) { - connect.openConnectModal?.(); - return; - } - const managedProfiles = await lensClient.wallet.profilesManaged({ - for: address, - }); - const profiles: LensProfile[] = managedProfiles.items.map((p) => ({ - id: p.id, - handle: p.handle ? `${p.handle.localName}.lens` : undefined, - })); + const signer = lensSignerRef.current; + const currentAddress = addressRef.current; - if (!profiles[0]) { - throw new Error("No Lens profiles managed by connected address"); - } + if (signer) { + return; + } - if (managedProfiles.items.length > 1) { - setAvailableProfiles(profiles); - setShowProfileSelector(true); - } else { - await handleSelectProfile(profiles[0]); - } + if (!currentAddress) { + connectRef.current.openConnectModal?.(); + return; + } + + const managedProfiles = await lensClient.wallet.profilesManaged({ + for: currentAddress, + }); + const profiles: LensProfile[] = managedProfiles.items.map((p) => ({ + id: p.id, + handle: p.handle ? `${p.handle.localName}.lens` : undefined, + })); + + if (!profiles[0]) { + throw new Error("No Lens profiles managed by connected address"); + } + + if (managedProfiles.items.length > 1) { + setAvailableProfiles(profiles); + setShowProfileSelector(true); + } else { + await handleSelectProfile(profiles[0]); } } catch (error) { // eslint-disable-next-line no-console -- provide feedback @@ -202,22 +236,34 @@ export function useLensIdentity({ } finally { setIsLoading(false); } - }, [address, connect, handleSelectProfile, lensClient.wallet, lensSigner]); + }, [ + addressRef, + connectRef, + handleSelectProfile, + lensClient.wallet, + lensSignerRef, + ]); + + const activeConnectionRef = useFreshRef(activeConnection); const signFrameAction: SignFrameActionFunction< SignerStateActionContext, LensFrameRequest > = useCallback( async (actionContext) => { - if (!lensSigner) { + const signer = lensSignerRef.current; + + if (!signer) { throw new Error("No lens signer active"); } + const profileManagers = await lensClient.profile.managers({ - for: lensSigner.profileId, + for: signer.profileId, }); const lensManagerEnabled = profileManagers.items.some( (manager) => manager.isLensManager ); + if (lensManagerEnabled) { const result = await lensClient.frames.signFrameAction({ url: actionContext.url, @@ -226,7 +272,7 @@ export function useLensIdentity({ buttonIndex: actionContext.buttonIndex, actionResponse: actionContext.type === "tx-post" ? actionContext.transactionId : "", - profileId: lensSigner.profileId, + profileId: signer.profileId, pubId: actionContext.frameContext.pubId || "", specVersion: "1.0.0", }); @@ -249,7 +295,7 @@ export function useLensIdentity({ clientProtocol: "lens@1.0.0", untrustedData: { ...result.value.signedTypedData.value, - identityToken: lensSigner.identityToken, + identityToken: signer.identityToken, unixTimestamp: Date.now(), }, trustedData: { @@ -267,17 +313,19 @@ export function useLensIdentity({ buttonIndex: actionContext.buttonIndex, actionResponse: actionContext.type === "tx-post" ? actionContext.transactionId : "", - profileId: lensSigner.profileId, + profileId: signer.profileId, pubId: actionContext.frameContext.pubId || "", specVersion: "1.0.0", deadline: Math.floor(Date.now() / 1000) + 86400, // 1 day }); - if (activeConnection[0]?.chainId !== typedData.domain.chainId) { - await switchChain(config, { chainId: typedData.domain.chainId }); + if ( + activeConnectionRef.current[0]?.chainId !== typedData.domain.chainId + ) { + await switchChainAsync({ chainId: typedData.domain.chainId }); } - const signature = await signTypedData(config, { + const signature = await signTypedDataAsync({ domain: { ...typedData.domain, verifyingContract: typedData.domain @@ -301,7 +349,7 @@ export function useLensIdentity({ clientProtocol: "lens@1.0.0", untrustedData: { ...typedData.value, - identityToken: lensSigner.identityToken, + identityToken: signer.identityToken, unixTimestamp: Date.now(), }, trustedData: { @@ -312,11 +360,12 @@ export function useLensIdentity({ }; }, [ - activeConnection, - config, + activeConnectionRef, lensClient.frames, lensClient.profile, - lensSigner, + lensSignerRef, + signTypedDataAsync, + switchChainAsync, ] ); @@ -324,18 +373,36 @@ export function useLensIdentity({ setShowProfileSelector(false); }, []); - return useMemo( - () => ({ + const isLoadingRef = useFreshRef(isLoading); + const availableProfilesRef = useFreshRef(availableProfiles); + + return useMemo(() => { + /** + * See the explanation in useFarcasterIdentity() + */ + void lensSigner; + void isLoading; + void availableProfiles; + + return { specification: "openframes", - signer: lensSigner, - hasSigner: !!lensSigner?.accessToken, + get signer() { + return lensSignerRef.current; + }, + get hasSigner() { + return !!lensSignerRef.current?.accessToken; + }, signFrameAction, - isLoadingSigner: isLoading, + get isLoadingSigner() { + return isLoadingRef.current; + }, onSignerlessFramePress, logout, showProfileSelector, closeProfileSelector, - availableProfiles, + get availableProfiles() { + return availableProfilesRef.current; + }, handleSelectProfile, withContext(frameContext) { return { @@ -343,17 +410,19 @@ export function useLensIdentity({ frameContext, }; }, - }), - [ - availableProfiles, - closeProfileSelector, - handleSelectProfile, - isLoading, - lensSigner, - logout, - onSignerlessFramePress, - showProfileSelector, - signFrameAction, - ] - ); + }; + }, [ + availableProfiles, + availableProfilesRef, + closeProfileSelector, + handleSelectProfile, + isLoading, + isLoadingRef, + lensSigner, + lensSignerRef, + logout, + onSignerlessFramePress, + showProfileSelector, + signFrameAction, + ]); } diff --git a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx index 7f0950648..91d29877b 100644 --- a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx +++ b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx @@ -3,8 +3,7 @@ import { type FramePostPayload, FramesClient } from "@xmtp/frames-client"; import { Client, type Signer } from "@xmtp/xmtp-js"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { zeroAddress } from "viem"; -import { useAccount, useConfig } from "wagmi"; -import { getAccount, signMessage } from "wagmi/actions"; +import { useAccount, useSignMessage } from "wagmi"; import type { Storage } from "../types"; import type { SignerStateActionContext, @@ -12,6 +11,7 @@ import type { SignFrameActionFunction, } from "../../types"; import { WebStorage } from "../storage"; +import { useFreshRef } from "../../hooks/use-fresh-ref"; import type { XmtpFrameContext } from "./use-xmtp-context"; export type XmtpSigner = { @@ -51,9 +51,9 @@ export function useXmtpIdentity({ const [isLoading, setIsLoading] = useState(false); const [xmtpSigner, setXmtpSigner] = useState(null); const [xmtpClient, setXmtpClient] = useState(null); - const config = useConfig(); const connect = useConnectModal(); const { address } = useAccount(); + const { signMessageAsync } = useSignMessage(); const walletSigner: Signer | null = useMemo( () => @@ -63,7 +63,7 @@ export function useXmtpIdentity({ return Promise.resolve(address); }, signMessage(message) { - return signMessage(config, { + return signMessageAsync({ message: { raw: typeof message === "string" @@ -74,7 +74,7 @@ export function useXmtpIdentity({ }, } : null, - [address, config] + [address, signMessageAsync] ); useEffect(() => { @@ -115,17 +115,27 @@ export function useXmtpIdentity({ setXmtpSigner(null); }, [storageKey]); + const walletSignerRef = useFreshRef(walletSigner); + const xmtpSignerRef = useFreshRef(xmtpSigner); + const connectRef = useFreshRef(connect); + const xmtpClientRef = useFreshRef(xmtpClient); + const addressRef = useFreshRef(address); + const storageKeyRef = useFreshRef(storageKey); + const onSignerlessFramePress = useCallback(async (): Promise => { try { + const wallet = walletSignerRef.current; + const signer = xmtpSignerRef.current; + setIsLoading(true); - if (!xmtpSigner) { - if (!walletSigner) { - connect.openConnectModal?.(); + if (!signer) { + if (!wallet) { + connectRef.current.openConnectModal?.(); return; } - const keys = await Client.getKeys(walletSigner, { + const keys = await Client.getKeys(wallet, { env: "dev", skipContactPublishing: true, persistConversations: false, @@ -134,12 +144,15 @@ export function useXmtpIdentity({ privateKeyOverride: keys, }); - const walletAddress = getAccount(config).address || zeroAddress; + const walletAddress = addressRef.current || zeroAddress; - await storageRef.current.set(storageKey, () => ({ - walletAddress, - keys: Buffer.from(keys).toString("hex"), - })); + await storageRef.current.set( + storageKeyRef.current, + () => ({ + walletAddress, + keys: Buffer.from(keys).toString("hex"), + }) + ); setXmtpSigner({ keys, @@ -153,22 +166,20 @@ export function useXmtpIdentity({ } finally { setIsLoading(false); } - }, [config, walletSigner, xmtpSigner, connect, storageKey]); + }, [walletSignerRef, xmtpSignerRef, addressRef, storageKeyRef, connectRef]); const signFrameAction: SignFrameActionFunction< SignerStateActionContext, FramePostPayload > = useCallback( async (actionContext) => { - if (!xmtpClient) { - throw new Error("No xmtp client"); - } + const client = xmtpClientRef.current; - if (!address) { - throw new Error("No address"); + if (!client) { + throw new Error("No xmtp client"); } - const framesClient = new FramesClient(xmtpClient); + const framesClient = new FramesClient(client); const payload = await framesClient.signFrameAction({ frameUrl: actionContext.url, inputText: actionContext.inputText, @@ -205,16 +216,30 @@ export function useXmtpIdentity({ searchParams, }; }, - [address, xmtpClient] + [xmtpClientRef] ); - return useMemo( - () => ({ + const isLoadingRef = useFreshRef(isLoading); + + return useMemo(() => { + /** + * See the explanation in useFarcasterIdentity() + */ + void xmtpSigner; + void isLoading; + + return { specification: "openframes", - signer: xmtpSigner, - hasSigner: !!xmtpSigner?.keys, + get signer() { + return xmtpSignerRef.current; + }, + get hasSigner() { + return !!xmtpSignerRef.current?.keys; + }, signFrameAction, - isLoadingSigner: isLoading, + get isLoadingSigner() { + return isLoadingRef.current; + }, onSignerlessFramePress, logout, withContext(frameContext) { @@ -223,7 +248,14 @@ export function useXmtpIdentity({ frameContext, }; }, - }), - [isLoading, logout, onSignerlessFramePress, signFrameAction, xmtpSigner] - ); + }; + }, [ + isLoading, + isLoadingRef, + logout, + onSignerlessFramePress, + signFrameAction, + xmtpSigner, + xmtpSignerRef, + ]); } diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index de949ecd7..a091951be 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -742,7 +742,44 @@ export type SignerComposerActionResult = { /** * Used to sign composer action */ -export type SignComposerActionFunc = ( +export type SignComposerActionFunction = ( signerPrivateKey: string, actionContext: SignerStateComposerActionContext ) => Promise; + +export type SignerStateCastActionContext = { + fid: number; + /** + * The id of the cast from which the user initiated the action + */ + castId: { + fid: number; + hash: `0x${string}`; + }; + /** + * Cast action post url + */ + postUrl: string; +}; + +export type SignerCastActionResult = { + untrustedData: { + fid: number; + url: string; + messageHash: `0x${string}`; + timestamp: number; + network: number; + buttonIndex: number; + }; + trustedData: { + messageBytes: string; + }; +}; + +/** + * Used to sign cast action + */ +export type SignCastActionFunction = ( + signerPrivateKey: string, + actionContext: SignerStateCastActionContext +) => Promise; diff --git a/packages/render/src/use-cast-action.ts b/packages/render/src/use-cast-action.ts new file mode 100644 index 000000000..8cc593477 --- /dev/null +++ b/packages/render/src/use-cast-action.ts @@ -0,0 +1,543 @@ +import type { + CastActionFrameResponse, + CastActionMessageResponse, +} from "frames.js/types"; +import { useCallback, useEffect, useMemo, useReducer, useRef } from "react"; +import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; +import { useFreshRef } from "./hooks/use-fresh-ref"; +import type { FarcasterSigner } from "./identity/farcaster"; +import type { FarcasterSignerState } from "./farcaster"; +import { + isCastActionFrameResponse, + isCastActionMessageResponse, + isParseFramesWithReportsResult, + mergeSearchParamsToUrl, + tryCall, + tryCallAsync, +} from "./helpers"; +import { CastActionUnexpectedResponseError } from "./errors"; +import type { OnErrorFunction } from "./unstable-types"; + +type CastID = { + fid: number; + hash: `0x${string}`; +}; + +type FetchCastActionFunctionArg = { + proxyUrl: string; + signer: FarcasterSigner | null; + /** + * Cast action postUrl + */ + postUrl: string; + /** + * The id of the cast from which the user initiated the action + */ + castId: CastID; +}; + +type FetchCastActionFunction = ( + arg: FetchCastActionFunctionArg +) => Promise; + +export type UseCastActionOptions = { + /** + * The id of the cast from which the user initiated the action + * + * If value changes it will refetch the cast action + */ + castId: CastID; + /** + * Post URL of the action handler + * + * If value changes it will refetch the cast action. + */ + postUrl: string; + /** + * URL to the action proxy server. If value changes cast action will be refetched. + * + * Proxy must handle POST requests. + */ + proxyUrl: string; + /** + * Signer used to sign the cast action. + * + * If value changes it will refetch the cast action. + */ + signer: FarcasterSignerState; + /** + * If enabled it will fetch the cast action on mount. + * + * @defaultValue true + */ + enabled?: boolean; + /** + * Custom fetch function to fetch cast action. + */ + fetch?: (url: string, init: RequestInit) => Promise; + onError?: OnErrorFunction; + /** + * Called when the response is a CastActionMessageResponse. + */ + onMessageResponse?: (response: CastActionMessageResponse) => void; + /** + * Called when the response is a CastActionFrameResponse. + */ + onFrameResponse?: (response: CastActionFrameResponse) => void; + /** + * Called when the response is successful but it's not a valid cast action response. + */ + onInvalidResponse?: (response: unknown) => void; +}; + +type UseCastActionResult = { + refetch: () => Promise; +} & ( + | { + status: "idle"; + data: undefined; + error: undefined; + } + | { + status: "loading"; + data: undefined; + error: undefined; + } + | { + status: "error"; + data: undefined; + error: Error; + } + | { + status: "success"; + type: "frame"; + data: CastActionFrameResponse; + frame: ParseFramesWithReportsResult; + error: undefined; + } + | { + status: "success"; + type: "message"; + data: CastActionMessageResponse; + error: undefined; + } +); + +export function useCastAction({ + castId, + enabled = true, + fetch: fetchFunction = defaultFetchFn, + onError, + proxyUrl, + signer, + postUrl, + onFrameResponse, + onMessageResponse, + onInvalidResponse, +}: UseCastActionOptions): UseCastActionResult { + const onErrorRef = useFreshRef(onError); + const fetchRef = useFreshRef(fetchFunction); + const onFrameResponseRef = useFreshRef(onFrameResponse); + const onMessageResponseRef = useFreshRef(onMessageResponse); + const onInvalidResponseRef = useFreshRef(onInvalidResponse); + const signerRef = useFreshRef(signer); + const lastFetchActionArgRef = useRef(null); + const [state, dispatch] = useReducer(castActionReducer, { + status: "idle", + enabled, + }); + const castIdRef = useFreshRef(castId); + + const fetchAction = useCallback( + async (arg) => { + const currentSigner = arg.signer; + + if ( + currentSigner?.status !== "approved" && + currentSigner?.status !== "impersonating" + ) { + await signerRef.current.onSignerlessFramePress(); + return; + } + + dispatch({ type: "loading-url" }); + + const signedDataOrError = await tryCallAsync(() => + signerRef.current.signCastAction(currentSigner.privateKey, { + postUrl: arg.postUrl, + fid: currentSigner.fid, + castId: arg.castId, + }) + ); + + if (signedDataOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(signedDataOrError)); + dispatch({ type: "error", error: signedDataOrError }); + + return; + } + + const proxiedUrl = mergeSearchParamsToUrl( + arg.proxyUrl, + new URLSearchParams({ postUrl: arg.postUrl }) + ); + + const actionResponseOrError = await tryCallAsync(() => + fetchRef.current(proxiedUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(signedDataOrError), + cache: "no-cache", + }) + ); + + if (actionResponseOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(actionResponseOrError)); + dispatch({ type: "error", error: actionResponseOrError }); + + return; + } + + if (!actionResponseOrError.ok) { + const error = new Error( + `Unexpected response status ${actionResponseOrError.status}` + ); + + tryCall(() => onErrorRef.current?.(error)); + dispatch({ type: "error", error }); + + return; + } + + const actionResponseDataOrError = await tryCallAsync( + () => actionResponseOrError.clone().json() as Promise + ); + + if (actionResponseDataOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(actionResponseDataOrError)); + dispatch({ type: "error", error: actionResponseDataOrError }); + + return; + } + + if (isCastActionFrameResponse(actionResponseDataOrError)) { + tryCall(() => onFrameResponseRef.current?.(actionResponseDataOrError)); + + const signedFrameDataOrError = await tryCallAsync(() => { + return signerRef.current.signFrameAction({ + buttonIndex: 1, + frameButton: { + action: "post", + label: "cast action", + post_url: actionResponseDataOrError.frameUrl, + target: actionResponseDataOrError.frameUrl, + }, + frameContext: { + castId: castIdRef.current, + }, + signer: currentSigner, + url: actionResponseDataOrError.frameUrl, + type: "default", + }); + }); + + if (signedFrameDataOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(signedFrameDataOrError)); + dispatch({ type: "error", error: signedFrameDataOrError }); + + return; + } + + const proxiedFrameUrl = mergeSearchParamsToUrl( + arg.proxyUrl, + signedFrameDataOrError.searchParams, + new URLSearchParams({ + multispecification: "true", + postUrl: actionResponseDataOrError.frameUrl, + }) + ); + + const frameResponseOrError = await tryCallAsync(() => { + return fetchRef.current(proxiedFrameUrl, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(signedFrameDataOrError.body), + cache: "no-cache", + }); + }); + + if (frameResponseOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(frameResponseOrError)); + dispatch({ type: "error", error: frameResponseOrError }); + + return; + } + + if (!frameResponseOrError.ok) { + const error = new Error( + `Unexpected response status ${frameResponseOrError.status}` + ); + + tryCall(() => onErrorRef.current?.(error)); + dispatch({ type: "error", error }); + + return; + } + + const frameResponseDataOrError = await tryCallAsync( + () => frameResponseOrError.clone().json() as Promise + ); + + if (frameResponseDataOrError instanceof Error) { + tryCall(() => onErrorRef.current?.(frameResponseDataOrError)); + dispatch({ type: "error", error: frameResponseDataOrError }); + + return; + } + + if (!isParseFramesWithReportsResult(frameResponseDataOrError)) { + const error = new Error("Invalid frame response"); + + tryCall(() => onErrorRef.current?.(error)); + dispatch({ type: "error", error }); + + return; + } + + dispatch({ + type: "frame", + response: actionResponseDataOrError, + frame: frameResponseDataOrError, + }); + + return; + } + + if (isCastActionMessageResponse(actionResponseDataOrError)) { + tryCall(() => + onMessageResponseRef.current?.(actionResponseDataOrError) + ); + + dispatch({ + type: "message", + response: actionResponseDataOrError, + }); + + return; + } + + tryCall(() => onInvalidResponseRef.current?.(actionResponseDataOrError)); + + const error = new CastActionUnexpectedResponseError(); + tryCall(() => onErrorRef.current?.(error)); + + dispatch({ type: "error", error }); + }, + [ + castIdRef, + fetchRef, + onErrorRef, + onFrameResponseRef, + onInvalidResponseRef, + onMessageResponseRef, + signerRef, + ] + ); + + const stateRef = useFreshRef(state); + const refetch = useCallback(() => { + if (!stateRef.current.enabled || !lastFetchActionArgRef.current) { + return Promise.resolve(); + } + + return fetchAction(lastFetchActionArgRef.current); + }, [fetchAction, stateRef]); + + useEffect(() => { + dispatch({ type: "enabled-change", enabled }); + }, [enabled]); + + useEffect(() => { + if (!enabled || !postUrl) { + return; + } + + lastFetchActionArgRef.current = { + signer: signer.signer as unknown as FarcasterSigner | null, + postUrl, + proxyUrl, + castId, + }; + + fetchAction(lastFetchActionArgRef.current).catch((e) => { + onErrorRef.current?.(e instanceof Error ? e : new Error(String(e))); + }); + }, [ + castId, + postUrl, + proxyUrl, + signer.signer, + enabled, + fetchAction, + onErrorRef, + ]); + + return useMemo(() => { + switch (state.status) { + case "idle": + return { + status: "idle", + data: undefined, + error: undefined, + refetch, + }; + case "loading": + return { + status: "loading", + data: undefined, + error: undefined, + refetch, + }; + case "error": + return { + status: "error", + data: undefined, + error: state.error, + refetch, + }; + default: { + if (state.type === "frame") { + return { + status: "success", + type: "frame", + data: state.response, + frame: state.frame, + error: undefined, + refetch, + }; + } + + return { + status: "success", + type: "message", + data: state.response, + error: undefined, + refetch, + }; + } + } + }, [state, refetch]); +} + +function defaultFetchFn(url: string, init: RequestInit): Promise { + return fetch(url, init); +} + +type SharedCastActionReducerState = { + enabled: boolean; +}; + +type CastActionReducerState = SharedCastActionReducerState & + ( + | { + status: "idle"; + } + | { + status: "loading"; + } + | { + status: "error"; + error: Error; + } + | { + status: "success"; + type: "frame"; + response: CastActionFrameResponse; + frame: ParseFramesWithReportsResult; + } + | { + status: "success"; + type: "message"; + response: CastActionMessageResponse; + } + ); + +type CastActionReducerAction = + | { + type: "loading-url"; + } + | { + type: "error"; + error: Error; + } + | { + type: "frame"; + response: CastActionFrameResponse; + frame: ParseFramesWithReportsResult; + } + | { + type: "message"; + response: CastActionMessageResponse; + } + | { + type: "enabled-change"; + enabled: boolean; + }; + +function castActionReducer( + state: CastActionReducerState, + action: CastActionReducerAction +): CastActionReducerState { + if (action.type === "enabled-change") { + if (action.enabled) { + return { + ...state, + enabled: true, + }; + } + + return { + status: "idle", + enabled: false, + }; + } + + if (!state.enabled) { + return state; + } + + switch (action.type) { + case "frame": + return { + ...state, + status: "success", + type: "frame", + response: action.response, + frame: action.frame, + }; + case "message": + return { + ...state, + status: "success", + type: "message", + response: action.response, + }; + case "loading-url": + return { + ...state, + status: "loading", + }; + case "error": + return { + ...state, + status: "error", + error: action.error, + }; + default: + return state; + } +} From 842b0fd4dbad931400af0d033abc151158accdfa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 09:37:55 +0100 Subject: [PATCH 09/88] refactor: use new cast action hook and unstable frame hooks in debugger --- .../action-debugger-properties-table.tsx | 120 +++ .../app/components/action-debugger.tsx | 345 +-------- .../debugger/app/components/action-info.tsx | 54 ++ .../app/components/cast-action-debugger.tsx | 176 +++++ .../debugger/app/components/cast-composer.tsx | 13 +- .../components/composer-action-debugger.tsx | 15 +- .../components/frame-debugger-diagnostics.tsx | 187 +++++ .../frame-debugger-request-card-content.tsx | 135 ++++ .../frame-debugger-request-details.tsx | 14 +- .../app/components/frame-debugger.tsx | 552 ++++---------- .../app/components/protocol-config-button.tsx | 8 + .../app/components/shortened-text.tsx | 22 + packages/debugger/app/debugger-page.tsx | 717 +++++------------- .../app/hooks/useDebuggerFrameState.ts | 101 ++- .../app/hooks/useFarcasterIdentity.tsx | 17 +- .../app/hooks/useSharedFrameEventHandlers.tsx | 239 ++++++ .../providers/ProtocolSelectorProvider.tsx | 13 + 17 files changed, 1417 insertions(+), 1311 deletions(-) create mode 100644 packages/debugger/app/components/action-debugger-properties-table.tsx create mode 100644 packages/debugger/app/components/action-info.tsx create mode 100644 packages/debugger/app/components/cast-action-debugger.tsx create mode 100644 packages/debugger/app/components/frame-debugger-diagnostics.tsx create mode 100644 packages/debugger/app/components/frame-debugger-request-card-content.tsx create mode 100644 packages/debugger/app/components/shortened-text.tsx create mode 100644 packages/debugger/app/hooks/useSharedFrameEventHandlers.tsx create mode 100644 packages/debugger/app/providers/ProtocolSelectorProvider.tsx diff --git a/packages/debugger/app/components/action-debugger-properties-table.tsx b/packages/debugger/app/components/action-debugger-properties-table.tsx new file mode 100644 index 000000000..8a89203c1 --- /dev/null +++ b/packages/debugger/app/components/action-debugger-properties-table.tsx @@ -0,0 +1,120 @@ +import { useMemo } from "react"; +import type { CastActionDefinitionResponse } from "../frames/route"; +import type { ParsingReport } from "frames.js"; +import { Table, TableBody, TableCell, TableRow } from "@/components/table"; +import { AlertTriangleIcon, CheckCircle2Icon, XCircleIcon } from "lucide-react"; +import { cn } from "@/lib/utils"; +import { ShortenedText } from "./shortened-text"; + +function isPropertyExperimental([key, value]: [string, string]) { + return false; +} + +type ActionDebuggerPropertiesTableProps = { + actionMetadataItem: CastActionDefinitionResponse; +}; + +export function ActionDebuggerPropertiesTable({ + actionMetadataItem, +}: ActionDebuggerPropertiesTableProps) { + const properties = useMemo(() => { + /** tuple of key and value */ + const validProperties: [string, string][] = []; + /** tuple of key and error message */ + const invalidProperties: [string, ParsingReport[]][] = []; + const visitedInvalidProperties: string[] = []; + const result = actionMetadataItem; + + // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid + for (const [key, reports] of Object.entries(result.reports)) { + invalidProperties.push([key, reports]); + visitedInvalidProperties.push(key); + } + + for (const [key, value] of Object.entries(result.action)) { + if (visitedInvalidProperties.includes(key) || value == null) { + continue; + } + + if (typeof value === "object") { + validProperties.push([key, JSON.stringify(value)]); + } else { + validProperties.push([key, value]); + } + } + + return { + validProperties, + invalidProperties, + isValid: invalidProperties.length === 0, + hasExperimentalProperties: false, + }; + }, [actionMetadataItem]); + + return ( + + + {properties.validProperties.map(([propertyKey, value]) => { + return ( + + + {isPropertyExperimental([propertyKey, value]) ? ( + +
+ +
+
*
+
+ ) : ( + + )} +
+ {propertyKey} + + + +
+ ); + })} + {properties.invalidProperties.flatMap( + ([propertyKey, errorMessages]) => { + return errorMessages.map((errorMessage, i) => { + return ( + + + {errorMessage.level === "error" ? ( + + ) : ( + + )} + + {propertyKey} + +

+ {errorMessage.message} +

+
+
+ ); + }); + } + )} + {properties.hasExperimentalProperties && ( + + + *This property is experimental and may not have been adopted in + clients yet + + + )} +
+
+ ); +} diff --git a/packages/debugger/app/components/action-debugger.tsx b/packages/debugger/app/components/action-debugger.tsx index 851358855..2f5156308 100644 --- a/packages/debugger/app/components/action-debugger.tsx +++ b/packages/debugger/app/components/action-debugger.tsx @@ -1,184 +1,19 @@ -import { Table, TableBody, TableCell, TableRow } from "@/components/table"; -import { Card, CardContent } from "@/components/ui/card"; -import { - HoverCard, - HoverCardContent, - HoverCardTrigger, -} from "@/components/ui/hover-card"; -import { cn } from "@/lib/utils"; -import { - type FarcasterFrameContext, - type FrameActionBodyPayload, - defaultTheme, -} from "@frames.js/render"; -import { ParsingReport } from "frames.js"; -import { - AlertTriangle, - CheckCircle2, - InfoIcon, - RefreshCwIcon, - XCircle, -} from "lucide-react"; import React, { type Dispatch, type SetStateAction, useEffect, useImperativeHandle, - useMemo, useState, } from "react"; -import { Button } from "../../@/components/ui/button"; -import { FrameDebugger } from "./frame-debugger"; -import IconByName from "./octicons"; -import { MockHubActionContext } from "../utils/mock-hub-utils"; -import { useFrame } from "@frames.js/render/use-frame"; -import { WithTooltip } from "./with-tooltip"; -import { useToast } from "@/components/ui/use-toast"; +import type { MockHubActionContext } from "../utils/mock-hub-utils"; import type { CastActionDefinitionResponse } from "../frames/route"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import type { FarcasterSigner } from "@frames.js/render/identity/farcaster"; import { ComposerActionDebugger } from "./composer-action-debugger"; - -type FrameDebuggerFramePropertiesTableRowsProps = { - actionMetadataItem: CastActionDefinitionResponse; -}; - -function isPropertyExperimental([key, value]: [string, string]) { - // tx is experimental - return false; -} - -function ActionDebuggerPropertiesTableRow({ - actionMetadataItem, -}: FrameDebuggerFramePropertiesTableRowsProps) { - const properties = useMemo(() => { - /** tuple of key and value */ - const validProperties: [string, string][] = []; - /** tuple of key and error message */ - const invalidProperties: [string, ParsingReport[]][] = []; - const visitedInvalidProperties: string[] = []; - const result = actionMetadataItem; - - // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid - for (const [key, reports] of Object.entries(result.reports)) { - invalidProperties.push([key, reports]); - visitedInvalidProperties.push(key); - } - - for (const [key, value] of Object.entries(result.action)) { - if (visitedInvalidProperties.includes(key) || value == null) { - continue; - } - - if (typeof value === "object") { - validProperties.push([key, JSON.stringify(value)]); - } else { - validProperties.push([key, value]); - } - } - - return { - validProperties, - invalidProperties, - isValid: invalidProperties.length === 0, - hasExperimentalProperties: false, - }; - }, [actionMetadataItem]); - - return ( - - - {properties.validProperties.map(([propertyKey, value]) => { - return ( - - - {isPropertyExperimental([propertyKey, value]) ? ( - -
- -
-
*
-
- ) : ( - - )} -
- {propertyKey} - - - -
- ); - })} - {properties.invalidProperties.flatMap( - ([propertyKey, errorMessages]) => { - return errorMessages.map((errorMessage, i) => { - return ( - - - {errorMessage.level === "error" ? ( - - ) : ( - - )} - - {propertyKey} - -

- {errorMessage.message} -

-
-
- ); - }); - } - )} - {properties.hasExperimentalProperties && ( - - - *This property is experimental and may not have been adopted in - clients yet - - - )} -
-
- ); -} - -function ShortenedText({ - maxLength, - text, -}: { - maxLength: number; - text: string; -}) { - if (text.length < maxLength) return text; - - return ( - - {text.slice(0, maxLength - 3)}... - {text} - - ); -} +import { CastActionDebugger } from "./cast-action-debugger"; +import { useProtocolSelector } from "../providers/ProtocolSelectorProvider"; type ActionDebuggerProps = { actionMetadataItem: CastActionDefinitionResponse; - farcasterFrameConfig: Parameters< - typeof useFrame< - FarcasterSigner | null, - FrameActionBodyPayload, - FarcasterFrameContext - > - >[0]; refreshUrl: (arg0?: string) => void; mockHubContext?: Partial; setMockHubContext?: Dispatch>>; @@ -198,7 +33,6 @@ export const ActionDebugger = React.forwardRef< ( { actionMetadataItem, - farcasterFrameConfig, refreshUrl, mockHubContext, setMockHubContext, @@ -206,7 +40,7 @@ export const ActionDebugger = React.forwardRef< }, ref ) => { - const { toast } = useToast(); + const protocolSelector = useProtocolSelector(); const [activeTab, setActiveTab] = useState( "type" in actionMetadataItem.action && actionMetadataItem.action.type === "composer" @@ -214,6 +48,7 @@ export const ActionDebugger = React.forwardRef< : "cast-action" ); const [copySuccess, setCopySuccess] = useState(false); + useEffect(() => { if (copySuccess) { setTimeout(() => { @@ -222,14 +57,6 @@ export const ActionDebugger = React.forwardRef< } }, [copySuccess, setCopySuccess]); - const actionFrameState = useFrame({ - ...farcasterFrameConfig, - }); - const [castActionDefinition, setCastActionDefinition] = useState | null>(null); - useImperativeHandle( ref, () => { @@ -258,114 +85,24 @@ export const ActionDebugger = React.forwardRef< - refreshUrl()} - > -
-
-
-
- -
-
-
- {actionMetadataItem.action.name} -
-
- {actionMetadataItem.action.description} -
-
-
-
- - - -
-
- -
-
- - {!!castActionDefinition && - !("type" in castActionDefinition.action) && ( -
-
- -
- )} + mockHubContext={mockHubContext} + setMockHubContext={setMockHubContext} + />
- refreshUrl()} - > - { - setActiveTab("cast-action"); - }} - /> - + onToggleToCastActionDebugger={() => { + setActiveTab("cast-action"); + }} + /> @@ -374,51 +111,3 @@ export const ActionDebugger = React.forwardRef< ); ActionDebugger.displayName = "ActionDebugger"; - -type ActionInfoProps = { - actionMetadataItem: CastActionDefinitionResponse; - children: React.ReactNode; - onRefreshUrl: () => void; -}; - -function ActionInfo({ - actionMetadataItem, - children, - onRefreshUrl, -}: ActionInfoProps) { - return ( -
-
-
- Reload URL

}> - -
-
-
-
- - {children} - -
-
-
- - - - - -
-
-
- ); -} diff --git a/packages/debugger/app/components/action-info.tsx b/packages/debugger/app/components/action-info.tsx new file mode 100644 index 000000000..7b32bd105 --- /dev/null +++ b/packages/debugger/app/components/action-info.tsx @@ -0,0 +1,54 @@ +import { Button } from "@/components/ui/button"; +import type { CastActionDefinitionResponse } from "../frames/route"; +import { WithTooltip } from "./with-tooltip"; +import { RefreshCwIcon } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { ActionDebuggerPropertiesTable } from "./action-debugger-properties-table"; + +type ActionInfoProps = { + actionMetadataItem: CastActionDefinitionResponse; + children: React.ReactNode; + onRefreshUrl: () => void; +}; + +export function ActionInfo({ + actionMetadataItem, + children, + onRefreshUrl, +}: ActionInfoProps) { + return ( +
+
+
+ Reload URL

}> + +
+
+
+
+ + {children} + +
+
+
+ + + + + +
+
+
+ ); +} diff --git a/packages/debugger/app/components/cast-action-debugger.tsx b/packages/debugger/app/components/cast-action-debugger.tsx new file mode 100644 index 000000000..1e23c7d2d --- /dev/null +++ b/packages/debugger/app/components/cast-action-debugger.tsx @@ -0,0 +1,176 @@ +import { InfoIcon } from "lucide-react"; +import type { CastActionDefinitionResponse } from "../frames/route"; +import IconByName from "./octicons"; +import { useToast } from "@/components/ui/use-toast"; +import { ActionInfo } from "./action-info"; +import { defaultTheme, fallbackFrameContext } from "@frames.js/render"; +import { useCastAction } from "@frames.js/render/use-cast-action"; +import { FrameDebugger } from "./frame-debugger"; +import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; +import { type Dispatch, type SetStateAction, useState } from "react"; +import type { MockHubActionContext } from "../utils/mock-hub-utils"; + +type CastActionDebuggerProps = { + actionMetadataItem: CastActionDefinitionResponse; + onRefreshUrl: () => void; + mockHubContext?: Partial; + setMockHubContext?: Dispatch>>; + hasExamples: boolean; +}; + +export function CastActionDebugger({ + actionMetadataItem, + onRefreshUrl, + mockHubContext, + setMockHubContext, + hasExamples, +}: CastActionDebuggerProps) { + const { toast } = useToast(); + const farcasterIdentity = useFarcasterIdentity(); + const [postUrl, setPostUrl] = useState(null); + const castAction = useCastAction({ + ...(postUrl + ? { + enabled: true, + postUrl, + } + : { + enabled: false, + postUrl: "", + }), + castId: fallbackFrameContext.castId, + proxyUrl: "/frames", + signer: farcasterIdentity, + onInvalidResponse(response) { + console.error(response); + + toast({ + title: "Invalid action response", + description: + "Please check the browser developer console for more information", + variant: "destructive", + }); + }, + onMessageResponse(response) { + console.log(response); + toast({ + description: response.message, + }); + }, + onError(error) { + console.error(error); + + toast({ + title: "Unexpected error happened", + description: + "Please check the browser developer console for more information", + variant: "destructive", + }); + }, + }); + + return ( + <> + +
+
+
+
+ +
+
+
+ {actionMetadataItem.action.name} +
+
+ {actionMetadataItem.action.description} +
+
+
+
+ + + +
+
+ +
+
+ + {castAction.status === "success" && castAction.type === "frame" && ( + + )} + + ); +} diff --git a/packages/debugger/app/components/cast-composer.tsx b/packages/debugger/app/components/cast-composer.tsx index a0def6594..7d213dd86 100644 --- a/packages/debugger/app/components/cast-composer.tsx +++ b/packages/debugger/app/components/cast-composer.tsx @@ -12,6 +12,7 @@ import { } from "lucide-react"; import IconByName from "./octicons"; import { useFrame_unstable } from "@frames.js/render/use-frame"; +import { isValidPartialFrame } from "@frames.js/render/ui/utils"; import { WithTooltip } from "./with-tooltip"; import { fallbackFrameContext } from "@frames.js/render"; import { FrameUI } from "./frame-ui"; @@ -20,7 +21,6 @@ import { ToastAction } from "@radix-ui/react-toast"; import Link from "next/link"; import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; import { useAccount } from "wagmi"; -import { FrameStackDone } from "@frames.js/render/unstable-types"; import { useDebuggerFrameState } from "../hooks/useDebuggerFrameState"; type CastComposerProps = { @@ -122,15 +122,6 @@ function createDebugUrl(frameUrl: string, currentUrl: string) { return debugUrl.toString(); } -function isAtLeastPartialFrame(stackItem: FrameStackDone): boolean { - return ( - stackItem.frameResult.status === "success" || - (!!stackItem.frameResult.frame && - !!stackItem.frameResult.frame.buttons && - stackItem.frameResult.frame.buttons.length > 0) - ); -} - function CastEmbedPreview({ onRemove, url }: CastEmbedPreviewProps) { const account = useAccount(); const { toast } = useToast(); @@ -212,7 +203,7 @@ function CastEmbedPreview({ onRemove, url }: CastEmbedPreviewProps) { if ( frame.currentFrameStackItem?.status === "done" && - isAtLeastPartialFrame(frame.currentFrameStackItem) + isValidPartialFrame(frame.currentFrameStackItem.frameResult) ) { return (
diff --git a/packages/debugger/app/components/composer-action-debugger.tsx b/packages/debugger/app/components/composer-action-debugger.tsx index faa620999..ea1fdcb34 100644 --- a/packages/debugger/app/components/composer-action-debugger.tsx +++ b/packages/debugger/app/components/composer-action-debugger.tsx @@ -2,20 +2,26 @@ import type { ComposerActionResponse, ComposerActionState, } from "frames.js/types"; -import { CastComposer, CastComposerRef } from "./cast-composer"; +import { CastComposer, type CastComposerRef } from "./cast-composer"; import { useRef, useState } from "react"; import { ComposerFormActionDialog } from "./composer-form-action-dialog"; import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; +import { ActionInfo } from "./action-info"; +import type { CastActionDefinitionResponse } from "../frames/route"; type ComposerActionDebuggerProps = { url: string; + actionMetadataItem: CastActionDefinitionResponse; actionMetadata: Partial; onToggleToCastActionDebugger: () => void; + onRefreshUrl: () => void; }; export function ComposerActionDebugger({ actionMetadata, + actionMetadataItem, url, + onRefreshUrl, onToggleToCastActionDebugger, }: ComposerActionDebuggerProps) { const castComposerRef = useRef(null); @@ -25,7 +31,10 @@ export function ComposerActionDebugger({ ); return ( - <> + )} - + ); } diff --git a/packages/debugger/app/components/frame-debugger-diagnostics.tsx b/packages/debugger/app/components/frame-debugger-diagnostics.tsx new file mode 100644 index 000000000..7d9914fea --- /dev/null +++ b/packages/debugger/app/components/frame-debugger-diagnostics.tsx @@ -0,0 +1,187 @@ +import { Table, TableBody, TableCell, TableRow } from "@/components/table"; +import { + getFrameFlattened, + getFrameV2Flattened, + type ParsingReport, +} from "frames.js"; +import { AlertTriangleIcon, CheckCircle2Icon, XCircleIcon } from "lucide-react"; +import { useMemo } from "react"; +import { ShortenedText } from "./shortened-text"; +import type { DebuggerFrameStackItem } from "../hooks/useDebuggerFrameState"; +import { cn } from "@/lib/utils"; + +type FrameDebuggerDiagnosticsProps = { + stackItem: DebuggerFrameStackItem; +}; + +function isPropertyExperimental([key, value]: [string, string]) { + // tx is experimental + return false; +} + +export function FrameDebuggerDiagnostics({ + stackItem, +}: FrameDebuggerDiagnosticsProps) { + const properties = useMemo(() => { + /** tuple of key and value */ + const validProperties: [string, string][] = []; + /** tuple of key and error message */ + const invalidProperties: [string, ParsingReport[]][] = []; + const visitedInvalidProperties: string[] = []; + + if (stackItem.status === "pending") { + return { validProperties, invalidProperties, isValid: true }; + } + + if (stackItem.status === "requestError") { + return { validProperties, invalidProperties, isValid: false }; + } + + if (stackItem.status === "message") { + return { validProperties, invalidProperties, isValid: true }; + } + + if (stackItem.status === "doneRedirect") { + return { validProperties, invalidProperties, isValid: true }; + } + + const result = stackItem.frameResult; + + // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid + for (const [key, reports] of Object.entries(result.reports)) { + invalidProperties.push([key, reports]); + visitedInvalidProperties.push(key); + } + + const flattenedFrame = + result.specification === "farcaster_v2" + ? getFrameV2Flattened(result.frame, { + "frames.js:version": + "frames.js:version" in result.frame && + typeof result.frame["frames.js:version"] === "string" + ? result.frame["frames.js:version"] + : undefined, + }) + : getFrameFlattened(result.frame, { + "frames.js:version": + "frames.js:version" in result.frame && + typeof result.frame["frames.js:version"] === "string" + ? result.frame["frames.js:version"] + : undefined, + }); + + if (result.framesVersion) { + validProperties.push(["frames.js:version", result.framesVersion]); + } + + let hasExperimentalProperties = false; + + for (const [key, value] of Object.entries(flattenedFrame)) { + hasExperimentalProperties = + hasExperimentalProperties || isPropertyExperimental([key, value ?? ""]); + // skip if the key is already set as invalid or value is undefined / null + if (visitedInvalidProperties.includes(key) || value == null) { + continue; + } + + validProperties.push([key, value]); + } + + return { + validProperties, + invalidProperties, + isValid: invalidProperties.length === 0, + hasExperimentalProperties, + }; + }, [stackItem]); + + if (stackItem.status === "pending") { + return null; + } + + return ( + + + + + {stackItem.extra.speed > 5 ? ( + + ) : stackItem.extra.speed > 4 ? ( + + ) : ( + + )} + + frame speed + + {stackItem.extra.speed > 5 + ? `Request took more than 5s (${stackItem.extra.speed} seconds). This may be normal: first request will take longer in development (as next.js builds), but in production, clients will timeout requests after 5s` + : stackItem.extra.speed > 4 + ? `Warning: Request took more than 4s (${stackItem.extra.speed} seconds). Requests will fail at 5s. This may be normal: first request will take longer in development (as next.js builds), but in production, if there's variance here, requests could fail in production if over 5s` + : `${stackItem.extra.speed} seconds`} + + + {properties.validProperties.map(([propertyKey, value]) => { + return ( + + + {isPropertyExperimental([propertyKey, value]) ? ( + +
+ +
+
*
+
+ ) : ( + + )} +
+ {propertyKey} + + + +
+ ); + })} + {properties.invalidProperties.flatMap( + ([propertyKey, errorMessages]) => { + return errorMessages.map((errorMessage, i) => { + return ( + + + {errorMessage.level === "error" ? ( + + ) : ( + + )} + + {propertyKey} + +

+ {errorMessage.message} +

+
+
+ ); + }); + } + )} + {properties.hasExperimentalProperties && ( + + + *This property is experimental and may not have been adopted in + clients yet + + + )} +
+
+ ); +} diff --git a/packages/debugger/app/components/frame-debugger-request-card-content.tsx b/packages/debugger/app/components/frame-debugger-request-card-content.tsx new file mode 100644 index 000000000..1180dfe75 --- /dev/null +++ b/packages/debugger/app/components/frame-debugger-request-card-content.tsx @@ -0,0 +1,135 @@ +import type { UseFrameReturnValue } from "@frames.js/render/unstable-types"; +import type { + DebuggerFrameStack, + DebuggerFrameStackItem, +} from "../hooks/useDebuggerFrameState"; +import { + AlertTriangleIcon, + CheckCircle2Icon, + ExternalLinkIcon, + InfoIcon, + LoaderIcon, + XCircleIcon, +} from "lucide-react"; +import { hasWarnings } from "../lib/utils"; +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; +import { urlSearchParamsToObject } from "../utils/url-search-params-to-object"; + +type FrameDebuggerRequestCardContentProps = { + stack: DebuggerFrameStack; + fetchFrame: UseFrameReturnValue["fetchFrame"]; +}; + +export function FrameDebuggerRequestCardContent({ + fetchFrame, + stack, +}: FrameDebuggerRequestCardContentProps) { + return stack.map((frameStackItem, i) => { + return ( + + ); + }); +} + +const FramesRequestCardContentIcon: React.FC<{ + stackItem: DebuggerFrameStackItem; +}> = ({ stackItem }) => { + if (stackItem.status === "pending") { + return ; + } + + if (stackItem.status === "requestError") { + return ; + } + + if (stackItem.status === "message") { + if (stackItem.type === "info") { + return ; + } else { + return ; + } + } + + if (stackItem.status === "doneRedirect") { + return ; + } + + if (stackItem.frameResult?.status === "failure") { + return ; + } + + if (hasWarnings(stackItem.frameResult.reports)) { + return ; + } + + return ; +}; diff --git a/packages/debugger/app/components/frame-debugger-request-details.tsx b/packages/debugger/app/components/frame-debugger-request-details.tsx index 3400e3138..c5d4c6158 100644 --- a/packages/debugger/app/components/frame-debugger-request-details.tsx +++ b/packages/debugger/app/components/frame-debugger-request-details.tsx @@ -1,4 +1,3 @@ -import type { FramesStackItem } from "@frames.js/render"; import { JSONTree } from "react-json-tree"; import { Table, @@ -8,9 +7,10 @@ import { TableRow, } from "@/components/table"; import { urlSearchParamsToObject } from "../utils/url-search-params-to-object"; +import type { DebuggerFrameStackItem } from "../hooks/useDebuggerFrameState"; type FrameDebuggerRequestDetailsProps = { - frameStackItem: FramesStackItem; + frameStackItem: DebuggerFrameStackItem; }; export function FrameDebuggerRequestDetails({ @@ -48,7 +48,7 @@ export function FrameDebuggerRequestDetails({ Payload @@ -67,16 +67,16 @@ export function FrameDebuggerRequestDetails({ Response status - {frameStackItem.responseStatus} + {frameStackItem.extra.responseStatus} - {frameStackItem.response && ( + {frameStackItem.extra.response && ( Response headers { - /** tuple of key and value */ - const validProperties: [string, string][] = []; - /** tuple of key and error message */ - const invalidProperties: [string, ParsingReport[]][] = []; - const visitedInvalidProperties: string[] = []; - - if (stackItem.status === "pending") { - return { validProperties, invalidProperties, isValid: true }; - } - - if (stackItem.status === "requestError") { - return { validProperties, invalidProperties, isValid: false }; - } - - if (stackItem.status === "message") { - return { validProperties, invalidProperties, isValid: true }; - } - - if (stackItem.status === "doneRedirect") { - return { validProperties, invalidProperties, isValid: true }; - } - - const result = stackItem.frameResult; - - // we need to check validation errors first because getFrame incorrectly return a value for a key even if it's invalid - for (const [key, reports] of Object.entries(result.reports)) { - invalidProperties.push([key, reports]); - visitedInvalidProperties.push(key); - } - - const flattenedFrame = getFrameFlattened(result.frame, { - "frames.js:version": - "frames.js:version" in result.frame && - typeof result.frame["frames.js:version"] === "string" - ? result.frame["frames.js:version"] - : undefined, - }); - - if (result.framesVersion) { - validProperties.push(["frames.js:version", result.framesVersion]); - } - - let hasExperimentalProperties = false; - - for (const [key, value] of Object.entries(flattenedFrame)) { - hasExperimentalProperties = - hasExperimentalProperties || isPropertyExperimental([key, value ?? ""]); - // skip if the key is already set as invalid or value is undefined / null - if (visitedInvalidProperties.includes(key) || value == null) { - continue; - } - - validProperties.push([key, value]); - } - - return { - validProperties, - invalidProperties, - isValid: invalidProperties.length === 0, - hasExperimentalProperties, - }; - }, [stackItem]); - - if (stackItem.status === "pending") { - return null; - } - - return ( - - - - - {stackItem.speed > 5 ? ( - - ) : stackItem.speed > 4 ? ( - - ) : ( - - )} - - frame speed - - {stackItem.speed > 5 - ? `Request took more than 5s (${stackItem.speed} seconds). This may be normal: first request will take longer in development (as next.js builds), but in production, clients will timeout requests after 5s` - : stackItem.speed > 4 - ? `Warning: Request took more than 4s (${stackItem.speed} seconds). Requests will fail at 5s. This may be normal: first request will take longer in development (as next.js builds), but in production, if there's variance here, requests could fail in production if over 5s` - : `${stackItem.speed} seconds`} - - - {properties.validProperties.map(([propertyKey, value]) => { - return ( - - - {isPropertyExperimental([propertyKey, value]) ? ( - -
- -
-
*
-
- ) : ( - - )} -
- {propertyKey} - - - -
- ); - })} - {properties.invalidProperties.flatMap( - ([propertyKey, errorMessages]) => { - return errorMessages.map((errorMessage, i) => { - return ( - - - {errorMessage.level === "error" ? ( - - ) : ( - - )} - - {propertyKey} - -

- {errorMessage.message} -

-
-
- ); - }); - } - )} - {properties.hasExperimentalProperties && ( - - - *This property is experimental and may not have been adopted in - clients yet - - - )} -
-
- ); -} - -function ShortenedText({ - maxLength, - text, -}: { - maxLength: number; - text: string; -}) { - if (text.length < maxLength) return text; - - return ( - - {text.slice(0, maxLength - 3)}... - {text} - - ); -} - -const FramesRequestCardContentIcon: React.FC<{ - stackItem: FramesStackItem; -}> = ({ stackItem }) => { - if (stackItem.status === "pending") { - return ; - } - - if (stackItem.status === "requestError") { - return ; - } - - if (stackItem.status === "message") { - if (stackItem.type === "info") { - return ; - } else { - return ; - } - } - - if (stackItem.status === "doneRedirect") { - return ; - } - - if (stackItem.frameResult?.status === "failure") { - return ; - } - - if (hasWarnings(stackItem.frameResult.reports)) { - return ; - } - - return ; -}; - -const FramesRequestCardContent: React.FC<{ - stack: FramesStack; - fetchFrame: FrameState< - FarcasterSigner | XmtpSigner | LensSigner | AnonymousSigner | null - >["fetchFrame"]; -}> = ({ fetchFrame, stack }) => { - return stack.map((frameStackItem, i) => { - return ( - - ); - }); -}; - -type FrameDebuggerSharedProps = { - specification: SupportedParsingSpecification; +import { useDebuggerFrameState } from "../hooks/useDebuggerFrameState"; +import { FrameDebuggerDiagnostics } from "./frame-debugger-diagnostics"; +import { FrameDebuggerRequestCardContent } from "./frame-debugger-request-card-content"; +import { useSharedFrameEventHandlers } from "../hooks/useSharedFrameEventHandlers"; +import { ProtocolConfiguration } from "./protocol-config-button"; +import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; +import { + useXmtpFrameContext, + useXmtpIdentity, +} from "@frames.js/render/identity/xmtp"; +import { + useLensFrameContext, + useLensIdentity, +} from "@frames.js/render/identity/lens"; +import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; +import { useFarcasterFrameContext } from "@frames.js/render/identity/farcaster"; +import { useAccount } from "wagmi"; +import { zeroAddress } from "viem"; +import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; + +const anonymousFrameContext = {}; + +type FrameDebuggerProps = { url: string; mockHubContext?: Partial; setMockHubContext?: Dispatch>>; hasExamples: boolean; + protocol: ProtocolConfiguration; + initialFrame?: ParseFramesWithReportsResult; }; -type FrameDebuggerProps = FrameDebuggerSharedProps & - ( - | { - useFrameHook: () => FrameState; - } - | { - frameState: FrameState; - } - ); - export type FrameDebuggerRef = { showConsole(): void; }; @@ -389,18 +83,108 @@ export const FrameDebugger = React.forwardRef< ( { hasExamples, - specification, url, mockHubContext, setMockHubContext, - ...restProps + protocol, + initialFrame, }, ref ) => { - const frameState = - "useFrameHook" in restProps - ? restProps.useFrameHook() - : restProps.frameState; + const account = useAccount(); + const farcasterSignerState = useFarcasterIdentity(); + const xmtpSignerState = useXmtpIdentity(); + const lensSignerState = useLensIdentity(); + const anonymousSignerState = useAnonymousIdentity(); + + const farcasterFrameContext = useFarcasterFrameContext({ + fallbackContext: fallbackFrameContext, + }); + + const xmtpFrameContext = useXmtpFrameContext({ + fallbackContext: { + conversationTopic: "test", + participantAccountAddresses: account.address + ? [account.address, zeroAddress] + : [zeroAddress], + }, + }); + + const lensFrameContext = useLensFrameContext({ + fallbackContext: { + pubId: "0x01-0x01", + }, + }); + + const sharedFrameEventHandlers = useSharedFrameEventHandlers({ + debuggerRef: null, + }); + + const frameState = useFrame({ + ...sharedFrameEventHandlers, + frame: initialFrame, + homeframeUrl: url, + frameActionProxy: "/frames", + frameGetProxy: "/frames", + frameStateHook: useDebuggerFrameState, + extraButtonRequestPayload: { mockData: mockHubContext }, + transactionDataSuffix: + process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID && + (protocol.protocol === "farcaster" || + protocol.protocol === "farcaster_v2") + ? attribution( + parseInt(process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID) + ) + : undefined, + resolveSigner() { + switch (protocol.protocol) { + case "farcaster": + // it creates copies of the signer which is bad because if the signer is internally + // updated, we see only old value although the signer is updated. This is true only for public properties + // probably getters will be better in this regard? + return farcasterSignerState.withContext( + farcasterFrameContext.frameContext + ); + case "farcaster_v2": + return farcasterSignerState.withContext( + farcasterFrameContext.frameContext + ); + case "xmtp": + return xmtpSignerState.withContext(xmtpFrameContext.frameContext); + case "lens": + return lensSignerState.withContext(lensFrameContext.frameContext); + case "anonymous": + return anonymousSignerState.withContext(anonymousFrameContext); + default: + throw new Error(`Unknown protocol`); + } + }, + onError(error) { + console.error(error); + + toast({ + title: "Error occurred", + description: ( +
+

{error.message}

+

Please check the console for more information

+
+ ), + variant: "destructive", + action: ( + { + wantsToScrollConsoleToBottomRef.current = true; + setActiveTab("console"); + }} + > + Show console + + ), + }); + }, + }); const { toast } = useToast(); const debuggerConsoleTabRef = useRef(null); const [activeTab, setActiveTab] = useState("diagnostics"); @@ -416,27 +200,10 @@ export const FrameDebugger = React.forwardRef< } }, [copySuccess, setCopySuccess]); - const [openAccordions, setOpenAccordions] = useState([]); - const { currentFrameStackItem } = frameState; const isLoading = currentFrameStackItem?.status === "pending"; - useEffect(() => { - if (!isLoading) { - // make sure the first frame is open - if ( - !openAccordions.includes( - String(currentFrameStackItem?.timestamp.getTime()) - ) - ) - setOpenAccordions((v) => [ - ...v, - String(currentFrameStackItem?.timestamp.getTime()), - ]); - } - }, [isLoading, currentFrameStackItem?.timestamp, openAccordions]); - /** * This handles the case where the user clicks on the console button in toast, in that case he wants to scroll to the bottom * otherwise we should keep the scroll position as is. @@ -538,19 +305,19 @@ export const FrameDebugger = React.forwardRef<
- + /> - {specification === "farcaster" && + {protocol.specification === "farcaster" && mockHubContext && setMockHubContext && ( + /> )} @@ -621,6 +388,10 @@ export const FrameDebugger = React.forwardRef< )}
{currentFrameStackItem?.status === "done" && + (currentFrameStackItem.frameResult.specification === + "farcaster" || + currentFrameStackItem.frameResult.specification === + "openframes") && currentFrameStackItem.frameResult.frame.buttons ?.filter( (button) => @@ -683,9 +454,9 @@ export const FrameDebugger = React.forwardRef< Meta Tags - + /> + /> + /> {currentFrameStackItem.status === "done" ? ( @@ -725,9 +496,14 @@ export const FrameDebugger = React.forwardRef< // Copy the text inside the text field navigator.clipboard.writeText( - getFrameHtmlHead( - currentFrameStackItem.frameResult.frame - ) + currentFrameStackItem.frameResult + .specification === "farcaster_v2" + ? getFrameV2HtmlHead( + currentFrameStackItem.frameResult.frame + ) + : getFrameHtmlHead( + currentFrameStackItem.frameResult.frame + ) ); setCopySuccess(true); }} @@ -744,19 +520,25 @@ export const FrameDebugger = React.forwardRef< borderRadius: "4px", }} > - {getFrameHtmlHead( - "sourceFrame" in currentFrameStackItem.request && - currentFrameStackItem.request.sourceFrame - ? currentFrameStackItem.request.sourceFrame - : currentFrameStackItem.frameResult.frame - ) - .split(" !!t) - // hacky... - .flatMap((el, i) => [ - {`, -
, - ])} + {currentFrameStackItem.frameResult.specification === + "farcaster_v2" + ? getFrameV2HtmlHead( + currentFrameStackItem.frameResult.frame + ) + : getFrameHtmlHead( + "sourceFrame" in + currentFrameStackItem.request && + currentFrameStackItem.request.sourceFrame + ? currentFrameStackItem.request.sourceFrame + : currentFrameStackItem.frameResult.frame + ) + .split(" !!t) + // hacky... + .flatMap((el, i) => [ + {`, +
, + ])}
) : null} diff --git a/packages/debugger/app/components/protocol-config-button.tsx b/packages/debugger/app/components/protocol-config-button.tsx index 91853d538..a393edd6d 100644 --- a/packages/debugger/app/components/protocol-config-button.tsx +++ b/packages/debugger/app/components/protocol-config-button.tsx @@ -29,6 +29,10 @@ export type ProtocolConfiguration = protocol: "farcaster"; specification: "farcaster"; } + | { + protocol: "farcaster_v2"; + specification: "farcaster_v2"; + } | { protocol: "lens"; specification: "openframes"; @@ -47,6 +51,10 @@ export const protocolConfigurationMap: Record = { protocol: "farcaster", specification: "farcaster", }, + farcaster_v2: { + protocol: "farcaster_v2", + specification: "farcaster_v2", + }, xmtp: { protocol: "xmtp", specification: "openframes", diff --git a/packages/debugger/app/components/shortened-text.tsx b/packages/debugger/app/components/shortened-text.tsx new file mode 100644 index 000000000..84f23fb31 --- /dev/null +++ b/packages/debugger/app/components/shortened-text.tsx @@ -0,0 +1,22 @@ +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from "@/components/ui/hover-card"; + +export function ShortenedText({ + maxLength, + text, +}: { + maxLength: number; + text: string; +}) { + if (text.length < maxLength) return text; + + return ( + + {text.slice(0, maxLength - 3)}... + {text} + + ); +} diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index cd390136b..fee388f2d 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -3,20 +3,8 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; -import { - type UseFrameOptions, - fallbackFrameContext, - type OnTransactionFunc, - type OnSignatureFunc, - type FrameActionBodyPayload, - type OnConnectWalletFunc, - type FrameContext, - type FarcasterFrameContext, -} from "@frames.js/render"; -import { attribution } from "@frames.js/render/farcaster"; -import { useFrame } from "@frames.js/render/use-frame"; -import { ConnectButton, useConnectModal } from "@rainbow-me/rainbowkit"; -import { sendTransaction, signTypedData, switchChain } from "@wagmi/core"; +import { fallbackFrameContext } from "@frames.js/render"; +import { ConnectButton } from "@rainbow-me/rainbowkit"; import { useRouter } from "next/navigation"; import React, { useCallback, @@ -26,11 +14,14 @@ import React, { useState, } from "react"; import { zeroAddress } from "viem"; -import { useAccount, useChainId, useConfig } from "wagmi"; +import { useAccount } from "wagmi"; import pkg from "../package.json"; -import { FrameDebugger, FrameDebuggerRef } from "./components/frame-debugger"; +import { + FrameDebugger, + type FrameDebuggerRef, +} from "./components/frame-debugger"; import { LOCAL_STORAGE_KEYS } from "./constants"; -import { MockHubActionContext } from "./utils/mock-hub-utils"; +import type { MockHubActionContext } from "./utils/mock-hub-utils"; import { type ProtocolConfiguration, protocolConfigurationMap, @@ -38,7 +29,7 @@ import { } from "./components/protocol-config-button"; import { ActionDebugger, - ActionDebuggerRef, + type ActionDebuggerRef, } from "./components/action-debugger"; import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; import { Loader2 } from "lucide-react"; @@ -54,10 +45,7 @@ import type { FrameDefinitionResponse, } from "./frames/route"; import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; -import { - useFarcasterFrameContext, - type FarcasterSigner, -} from "@frames.js/render/identity/farcaster"; +import { useFarcasterFrameContext } from "@frames.js/render/identity/farcaster"; import { useLensFrameContext, useLensIdentity, @@ -67,15 +55,11 @@ import { useXmtpIdentity, } from "@frames.js/render/identity/xmtp"; import { useFarcasterIdentity } from "./hooks/useFarcasterIdentity"; -import { InvalidChainIdError, parseChainId } from "./lib/utils"; +import { ProtocolSelectorProvider } from "./providers/ProtocolSelectorProvider"; const FALLBACK_URL = process.env.NEXT_PUBLIC_DEBUGGER_DEFAULT_URL || "http://localhost:3000"; -class CouldNotChangeChainError extends Error {} - -const anonymousFrameContext = {}; - export default function DebuggerPage({ searchParams, examples, @@ -123,10 +107,7 @@ export default function DebuggerPage({ likedCast: false, recastedCast: false, }); - const currentChainId = useChainId(); - const config = useConfig(); const account = useAccount(); - const { openConnectModal } = useConnectModal(); useEffect(() => { const selectedProtocol = localStorage.getItem( @@ -245,9 +226,7 @@ export default function DebuggerPage({ refreshUrl(url); }, [url, protocolConfiguration, refreshUrl, toast, debuggerConsole]); - const farcasterSignerState = useFarcasterIdentity({ - selectProtocolButtonRef, - }); + const farcasterSignerState = useFarcasterIdentity(); const xmtpSignerState = useXmtpIdentity(); const lensSignerState = useLensIdentity(); const anonymousSignerState = useAnonymousIdentity(); @@ -271,542 +250,204 @@ export default function DebuggerPage({ }, }); - const onConnectWallet: OnConnectWalletFunc = useCallback(async () => { - if (!openConnectModal) { - throw new Error(`openConnectModal not implemented`); - } - - openConnectModal(); - }, [openConnectModal]); - - const onTransaction: OnTransactionFunc = useCallback( - async ({ transactionData }) => { - try { - const { params, chainId } = transactionData; - const requestedChainId = parseChainId(chainId); - - if (currentChainId !== requestedChainId) { - await switchChain(config, { - chainId: requestedChainId, - }).catch((e) => { - throw new CouldNotChangeChainError(e.message); - }); - } - - // Send the transaction - const transactionId = await sendTransaction(config, { - to: params.to, - data: params.data, - value: BigInt(params.value || 0), - }); - return transactionId; - } catch (error) { - let title: string; - - if (error instanceof InvalidChainIdError) { - title = "Invalid chain id"; - } else if (error instanceof CouldNotChangeChainError) { - title = "Could not change chain"; - } else { - title = "Error sending transaction"; - } - - toast({ - title, - description: "Please check the console for more information", - variant: "destructive", - action: debuggerRef.current ? ( - { - debuggerRef.current?.showConsole(); - }} - > - Show console - - ) : undefined, - }); - - console.error(error); - - return null; - } - }, - [currentChainId, config, toast] - ); - - const onSignature: OnSignatureFunc = useCallback( - async ({ signatureData }) => { - if (!account.address) { - openConnectModal?.(); - console.info( - "Opened connect modal because the account address is not set" - ); - - return null; - } - - try { - const { params, chainId } = signatureData; - const requestedChainId = parseChainId(chainId); - - if (currentChainId !== requestedChainId) { - await switchChain(config, { - chainId: requestedChainId, - }).catch((e) => { - throw new CouldNotChangeChainError(e.message); - }); - } - - // Sign the data - return await signTypedData(config, params); - } catch (error) { - let title: string; - - if (error instanceof InvalidChainIdError) { - title = "Invalid chain id"; - } else if (error instanceof CouldNotChangeChainError) { - title = "Could not change chain"; - } else { - title = "Error signing data"; - } - - toast({ - title, - description: "Please check the console for more information", - variant: "destructive", - action: debuggerRef.current ? ( - { - debuggerRef.current?.showConsole(); - }} - > - Show console - - ) : undefined, - }); - - console.error(error); - - return null; - } - }, - [account.address, currentChainId, config, openConnectModal, toast] - ); - - const useFrameConfig: Omit< - UseFrameOptions< - Record, - FrameActionBodyPayload, - FrameContext - >, - "signerState" | "specification" | "frameContext" - > = useMemo( - () => ({ - homeframeUrl: url, - frame: - initialFrame?.[protocolConfiguration?.specification ?? "farcaster"], - frameActionProxy: "/frames", - frameGetProxy: "/frames", - connectedAddress: account.address, - extraButtonRequestPayload: { mockData: mockHubContext }, - onTransaction, - onSignature, - onConnectWallet, - onError(error) { - console.error(error); - - if (actionDebuggerRef.current) { - if (error.message.includes("Must be called from composer")) { - toast({ - title: "Error occurred", - description: - "It seems that you tried to call a composer action in the cast action debugger.", - variant: "destructive", - action: ( - { - actionDebuggerRef.current?.switchTo("composer-action"); - }} - > - Switch - - ), - }); - - return; - } else if ( - error.message.includes( - "Unexpected composer action response from the server" - ) - ) { - toast({ - title: "Error occurred", - description: - "It seems that you tried to call a cast action in the composer action debugger.", - variant: "destructive", - action: ( - { - actionDebuggerRef.current?.switchTo("cast-action"); - }} - > - Switch - - ), - }); - - return; - } - } - - toast({ - title: "Error occurred", - description: ( -
-

{error.message}

-

Please check the console for more information

-
- ), - variant: "destructive", - action: debuggerRef.current ? ( - { - debuggerRef.current?.showConsole(); - }} - > - Show console - - ) : undefined, - }); - }, - onMint(t) { - if (!confirm(`Mint ${t.target}?`)) { - return; - } - - if (!account.address) { - openConnectModal?.(); - return; - } - - const searchParams = new URLSearchParams({ - target: t.target, - taker: account.address, - }); - - fetch(`/mint?${searchParams.toString()}`) - .then(async (res) => { - if (!res.ok) { - const json = await res.json(); - throw new Error(json.message); - } - return await res.json(); - }) - .then((json) => { - onTransaction({ ...t, transactionData: json.data }); - }) - .catch((e) => { - toast({ - title: "Error minting", - description: "Please check the console for more information", - variant: "destructive", - action: debuggerRef.current ? ( - { - debuggerRef.current?.showConsole(); - }} - > - Show console - - ) : undefined, - }); - console.error(e); - }); - }, - }), - [ - account.address, - initialFrame, - mockHubContext, - onConnectWallet, - onSignature, - onTransaction, - openConnectModal, - toast, - url, - protocolConfiguration?.specification, - ] - ); - - const farcasterFrameConfig: UseFrameOptions< - FarcasterSigner | null, - FrameActionBodyPayload, - FarcasterFrameContext - > = useMemo(() => { - const attributionData = process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID - ? attribution(parseInt(process.env.NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID)) - : undefined; + const selectProtocolContextValue = useMemo(() => { return { - ...useFrameConfig, - signerState: farcasterSignerState, - specification: "farcaster", - frameContext: { - ...fallbackFrameContext, - ...farcasterFrameContext.frameContext, + open() { + selectProtocolButtonRef.current?.click(); }, - transactionDataSuffix: attributionData, }; - }, [ - farcasterFrameContext.frameContext, - farcasterSignerState, - useFrameConfig, - ]); - - const useFrameHook = useMemo(() => { - return () => { - const selectedProtocol = protocolConfiguration?.protocol ?? "farcaster"; - - switch (selectedProtocol) { - case "anonymous": { - // eslint-disable-next-line react-hooks/rules-of-hooks -- this is used as a hook in FrameDebugger - return useFrame({ - ...useFrameConfig, - signerState: anonymousSignerState, - specification: "openframes", - frameContext: anonymousFrameContext, - }); - } - case "lens": { - // eslint-disable-next-line react-hooks/rules-of-hooks -- this is used as a hook in FrameDebugger - return useFrame({ - ...useFrameConfig, - signerState: lensSignerState, - specification: "openframes", - frameContext: lensFrameContext.frameContext, - }); - } - case "xmtp": { - // eslint-disable-next-line react-hooks/rules-of-hooks -- this is used as a hook in FrameDebugger - return useFrame({ - ...useFrameConfig, - signerState: xmtpSignerState, - specification: "openframes", - frameContext: xmtpFrameContext.frameContext, - }); - } - default: { - // eslint-disable-next-line react-hooks/rules-of-hooks -- this is used as a hook in FrameDebugger - return useFrame(farcasterFrameConfig); - } - } - }; - }, [ - anonymousSignerState, - farcasterFrameConfig, - lensFrameContext.frameContext, - lensSignerState, - protocolConfiguration?.protocol, - useFrameConfig, - xmtpFrameContext.frameContext, - xmtpSignerState, - ]); + }, []); return ( - -
-
- - - - - - - - - - -
{ - e.preventDefault(); - - const newUrl = - new FormData(e.currentTarget).get("url")?.toString() || ""; - - if (!newUrl) { - toast({ - title: "Missing URL", - description: "Please provide a URL to debug", - variant: "destructive", - action: ( - { - urlInputRef.current?.focus(); - }} - type="button" - > - Fix - - ), - }); - return; - } - - try { - const parsedUrl = new URL(newUrl); - - if ( - parsedUrl.protocol !== "http:" && - parsedUrl.protocol !== "https:" - ) { - throw new Error("Invalid protocol"); - } - - if (!protocolConfiguration) { + + +
+
+ + + + + + + + + + + { + e.preventDefault(); + + const newUrl = + new FormData(e.currentTarget).get("url")?.toString() || ""; + + if (!newUrl) { toast({ - title: "Select Protocol", - description: "Please select a protocol to debug the URL", + title: "Missing URL", + description: "Please provide a URL to debug", variant: "destructive", action: ( { - selectProtocolButtonRef.current?.click(); + urlInputRef.current?.focus(); }} type="button" > - Select + Fix ), }); return; } - if (searchParams.url === parsedUrl.toString()) { - location.reload(); - } + try { + const parsedUrl = new URL(newUrl); + + if ( + parsedUrl.protocol !== "http:" && + parsedUrl.protocol !== "https:" + ) { + throw new Error("Invalid protocol"); + } + + if (!protocolConfiguration) { + toast({ + title: "Select Protocol", + description: "Please select a protocol to debug the URL", + variant: "destructive", + action: ( + { + selectProtocolButtonRef.current?.click(); + }} + type="button" + > + Select + + ), + }); + return; + } + + if (searchParams.url === parsedUrl.toString()) { + location.reload(); + } + + router.push( + `?url=${encodeURIComponent(parsedUrl.toString())}` + ); + } catch (e) { + toast({ + title: "Invalid URL", + description: + "URL must start with http:// or https:// and be in valid format", + variant: "destructive", + action: ( + { + urlInputRef.current?.focus(); + }} + type="button" + > + Fix + + ), + }); - router.push(`?url=${encodeURIComponent(parsedUrl.toString())}`); - } catch (e) { - toast({ - title: "Invalid URL", - description: - "URL must start with http:// or https:// and be in valid format", - variant: "destructive", - action: ( - { - urlInputRef.current?.focus(); - }} - type="button" - > - Fix - - ), - }); - - return; - } - }} - > - - - - - { - setProtocolConfiguration(spec); - }} - value={protocolConfiguration} - farcasterSignerState={farcasterSignerState} - xmtpSignerState={xmtpSignerState} - anonymousSignerState={anonymousSignerState} - farcasterFrameContext={farcasterFrameContext} - xmtpFrameContext={xmtpFrameContext} - ref={selectProtocolButtonRef} - lensFrameContext={lensFrameContext} - lensSignerState={lensSignerState} - > - -
- + + + + + { + setProtocolConfiguration(spec); + }} + value={protocolConfiguration} + farcasterSignerState={farcasterSignerState} + xmtpSignerState={xmtpSignerState} + anonymousSignerState={anonymousSignerState} + farcasterFrameContext={farcasterFrameContext} + xmtpFrameContext={xmtpFrameContext} + ref={selectProtocolButtonRef} + lensFrameContext={lensFrameContext} + lensSignerState={lensSignerState} + > + +
+ +
-
- {url ? ( - <> - {initialAction && ( -
- -
- )} - - {initialFrame && - !!protocolConfiguration?.protocol && - !!protocolConfiguration.specification && ( + {url ? ( + <> + {initialAction && ( +
+ +
+ )} + + {initialFrame && !!protocolConfiguration && ( + /> )} - - ) : ( - examples + + ) : ( + examples + )} +
+ {lensSignerState.showProfileSelector && ( + )} -
- {lensSignerState.showProfileSelector && ( - - )} - + + ); } diff --git a/packages/debugger/app/hooks/useDebuggerFrameState.ts b/packages/debugger/app/hooks/useDebuggerFrameState.ts index 554343953..adc803761 100644 --- a/packages/debugger/app/hooks/useDebuggerFrameState.ts +++ b/packages/debugger/app/hooks/useDebuggerFrameState.ts @@ -1,4 +1,6 @@ import type { + FramesStack, + FramesStackItem, FrameState, FrameStateAPI, UseFrameStateOptions, @@ -9,15 +11,12 @@ function computeDurationInSeconds(start: Date, end: Date): number { return Number(((end.getTime() - start.getTime()) / 1000).toFixed(2)); } -type ExtraPending = { - startTime: Date; +type DebuggerSharedResponseExtra = { + timestamp: Date; requestDetails: { body?: object; searchParams?: URLSearchParams; }; -}; - -type SharedResponseExtra = { response: Response; responseStatus: number; responseBody: unknown; @@ -27,40 +26,67 @@ type SharedResponseExtra = { speed: number; }; -type ExtraDone = SharedResponseExtra; +export type DebuggerExtraPending = Pick< + DebuggerSharedResponseExtra, + "timestamp" | "requestDetails" +> & { + timestamp: Date; + startTime: Date; +}; + +export type DebuggerExtraDone = DebuggerSharedResponseExtra; -type ExtraDoneRedirect = SharedResponseExtra; +export type DebuggerExtraDoneRedirect = DebuggerSharedResponseExtra; -type ExtraRequestError = Pick & { +export type DebuggerExtraRequestError = Pick< + DebuggerSharedResponseExtra, + "speed" | "timestamp" | "requestDetails" +> & { response: Response | null; responseStatus: number; responseBody: unknown; }; -type ExtraMessage = SharedResponseExtra; +export type DebuggerExtraMessage = DebuggerSharedResponseExtra; + +export type DebuggerFrameStackItem = FramesStackItem< + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage +>; + +export type DebuggerFrameStack = FramesStack< + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage +>; type DebuggerFrameState = FrameState< - ExtraPending, - ExtraDone, - ExtraDoneRedirect, - ExtraRequestError, - ExtraMessage + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage >; type DebuggerFrameStateAPI = FrameStateAPI< - ExtraPending, - ExtraDone, - ExtraDoneRedirect, - ExtraRequestError, - ExtraMessage + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage >; type DebuggerFrameStateOptions = Omit< UseFrameStateOptions< - ExtraPending, - ExtraDone, - ExtraDoneRedirect, - ExtraRequestError, - ExtraMessage + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage >, "resolveDoneExtra" >; @@ -69,21 +95,28 @@ export function useDebuggerFrameState( options: DebuggerFrameStateOptions ): [DebuggerFrameState, DebuggerFrameStateAPI] { return useFrameState< - ExtraPending, - ExtraDone, - ExtraDoneRedirect, - ExtraRequestError, - ExtraMessage + DebuggerExtraPending, + DebuggerExtraDone, + DebuggerExtraDoneRedirect, + DebuggerExtraRequestError, + DebuggerExtraMessage >({ ...options, + initialPendingExtra: { + requestDetails: {}, + timestamp: new Date(), + startTime: new Date(), + }, resolveGETPendingExtra() { return { + timestamp: new Date(), startTime: new Date(), requestDetails: {}, }; }, resolvePOSTPendingExtra(arg) { return { + timestamp: new Date(), startTime: new Date(), requestDetails: { body: arg.action.body, @@ -93,6 +126,8 @@ export function useDebuggerFrameState( }, resolveDoneExtra(arg) { return { + timestamp: arg.pendingItem.extra.timestamp, + requestDetails: arg.pendingItem.extra.requestDetails, response: arg.response.clone(), speed: computeDurationInSeconds( arg.pendingItem.extra.startTime, @@ -104,6 +139,8 @@ export function useDebuggerFrameState( }, resolveDoneRedirectExtra(arg) { return { + timestamp: arg.pendingItem.extra.timestamp, + requestDetails: arg.pendingItem.extra.requestDetails, speed: computeDurationInSeconds( arg.pendingItem.extra.startTime, arg.endTime @@ -115,6 +152,8 @@ export function useDebuggerFrameState( }, resolveDoneWithErrorMessageExtra(arg) { return { + timestamp: arg.pendingItem.extra.timestamp, + requestDetails: arg.pendingItem.extra.requestDetails, speed: computeDurationInSeconds( arg.pendingItem.extra.startTime, arg.endTime @@ -126,6 +165,8 @@ export function useDebuggerFrameState( }, resolveFailedExtra(arg) { return { + timestamp: arg.pendingItem.extra.timestamp, + requestDetails: arg.pendingItem.extra.requestDetails, speed: computeDurationInSeconds( arg.pendingItem.extra.startTime, arg.endTime @@ -137,6 +178,8 @@ export function useDebuggerFrameState( }, resolveFailedWithRequestErrorExtra(arg) { return { + timestamp: arg.pendingItem.extra.timestamp, + requestDetails: arg.pendingItem.extra.requestDetails, speed: computeDurationInSeconds( arg.pendingItem.extra.startTime, arg.endTime diff --git a/packages/debugger/app/hooks/useFarcasterIdentity.tsx b/packages/debugger/app/hooks/useFarcasterIdentity.tsx index b3a1f544b..1a79f0e1b 100644 --- a/packages/debugger/app/hooks/useFarcasterIdentity.tsx +++ b/packages/debugger/app/hooks/useFarcasterIdentity.tsx @@ -2,21 +2,18 @@ import { ToastAction } from "@/components/ui/toast"; import { useToast } from "@/components/ui/use-toast"; import { useFarcasterMultiIdentity } from "@frames.js/render/identity/farcaster"; import { WebStorage } from "@frames.js/render/identity/storage"; +import { useProtocolSelector } from "../providers/ProtocolSelectorProvider"; const sharedStorage = new WebStorage(); type Options = Omit< Parameters[0], "onMissingIdentity" -> & { - selectProtocolButtonRef?: React.RefObject; -}; +>; -export function useFarcasterIdentity({ - selectProtocolButtonRef, - ...options -}: Options = {}) { +export function useFarcasterIdentity(options: Options = {}) { const { toast } = useToast(); + const protocolSelector = useProtocolSelector(); return useFarcasterMultiIdentity({ ...(options ?? {}), @@ -27,17 +24,17 @@ export function useFarcasterIdentity({ description: "In order to test the buttons you need to select an identity first", variant: "destructive", - action: selectProtocolButtonRef?.current ? ( + action: ( { - selectProtocolButtonRef?.current?.click(); + protocolSelector.open(); }} type="button" > Select identity - ) : undefined, + ), }); }, }); diff --git a/packages/debugger/app/hooks/useSharedFrameEventHandlers.tsx b/packages/debugger/app/hooks/useSharedFrameEventHandlers.tsx new file mode 100644 index 000000000..bd3fb2d56 --- /dev/null +++ b/packages/debugger/app/hooks/useSharedFrameEventHandlers.tsx @@ -0,0 +1,239 @@ +import type { + OnSignatureFunction, + OnTransactionFunction, + OnMintFunction, + ResolveAddressFunction, +} from "@frames.js/render/unstable-types"; +import { useConnectModal } from "@rainbow-me/rainbowkit"; +import { + useAccount, + useChainId, + useSendTransaction, + useSignTypedData, + useSwitchChain, +} from "wagmi"; +import { InvalidChainIdError, parseChainId } from "../lib/utils"; +import { useToast } from "@/components/ui/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import type { FrameDebuggerRef } from "../components/frame-debugger"; + +export class CouldNotChangeChainError extends Error {} + +type UseSharedFrameEventHandlersReturn = { + onTransaction: OnTransactionFunction; + onSignature: OnSignatureFunction; + onMint: OnMintFunction; + resolveAddress: ResolveAddressFunction; +}; + +type UseSharedFrameEventHandlersOptions = { + debuggerRef: React.MutableRefObject | null; +}; + +/** + * This hook provides shared event handles for useFrame() hook like onMint, onTransaction, etc... + */ +export function useSharedFrameEventHandlers({ + debuggerRef, +}: UseSharedFrameEventHandlersOptions): UseSharedFrameEventHandlersReturn { + const account = useAccount(); + const currentChainId = useChainId(); + const { sendTransactionAsync } = useSendTransaction(); + const { signTypedDataAsync } = useSignTypedData(); + const { switchChainAsync } = useSwitchChain(); + const { openConnectModal } = useConnectModal(); + const { toast } = useToast(); + + const onTransaction: OnTransactionFunction = async function onTransaction({ + transactionData, + }) { + try { + const { params, chainId } = transactionData; + const requestedChainId = parseChainId(chainId); + + if (currentChainId !== requestedChainId) { + await switchChainAsync({ + chainId: requestedChainId, + }).catch((e) => { + throw new CouldNotChangeChainError(e.message); + }); + } + + // Send the transaction + const transactionId = await sendTransactionAsync({ + to: params.to, + data: params.data, + value: BigInt(params.value || 0), + }); + return transactionId; + } catch (error) { + let title: string; + + if (error instanceof InvalidChainIdError) { + title = "Invalid chain id"; + } else if (error instanceof CouldNotChangeChainError) { + title = "Could not change chain"; + } else { + title = "Error sending transaction"; + } + + if (debuggerRef?.current) { + toast({ + title, + description: "Please check the console for more information", + variant: "destructive", + action: ( + { + debuggerRef.current?.showConsole(); + }} + > + Show console + + ), + }); + } else { + toast({ + title, + description: + "Please check browser developer console for more information", + variant: "destructive", + }); + } + + console.error(error); + + return null; + } + }; + + return { + async resolveAddress() { + if (account.address) { + return account.address; + } + + if (!openConnectModal) { + throw new Error("openConnectModal is not available"); + } + + openConnectModal(); + + return null; + }, + async onMint(t) { + if (!window.confirm(`Mint ${t.target}?`)) { + return; + } + + if (!account.address) { + openConnectModal?.(); + return; + } + + const searchParams = new URLSearchParams({ + target: t.target, + taker: account.address, + }); + + try { + const response = await fetch(`/mint?${searchParams.toString()}`); + + if (!response.ok) { + const json = await response.json(); + + throw new Error(json.message); + } + + const json = await response.json(); + + await onTransaction({ ...t, transactionData: json.data }); + } catch (e) { + console.error(e); + + if (debuggerRef?.current) { + toast({ + title: "Error minting", + description: "Please check the console for more information", + variant: "destructive", + action: ( + { + debuggerRef.current?.showConsole(); + }} + > + Show console + + ), + }); + } else { + toast({ + title: "Error minting", + description: + "Please check browser developer console for more information", + variant: "destructive", + }); + } + } + }, + async onSignature({ signatureData }) { + try { + const { params, chainId } = signatureData; + const requestedChainId = parseChainId(chainId); + + if (currentChainId !== requestedChainId) { + await switchChainAsync({ + chainId: requestedChainId, + }).catch((e) => { + throw new CouldNotChangeChainError(e.message); + }); + } + + // Sign the data + return await signTypedDataAsync(params); + } catch (error) { + let title: string; + + if (error instanceof InvalidChainIdError) { + title = "Invalid chain id"; + } else if (error instanceof CouldNotChangeChainError) { + title = "Could not change chain"; + } else { + title = "Error signing data"; + } + + if (debuggerRef?.current) { + toast({ + title, + description: "Please check the console for more information", + variant: "destructive", + action: ( + { + debuggerRef.current?.showConsole(); + }} + > + Show console + + ), + }); + } else { + toast({ + title, + description: + "Please check browser developer console for more information", + variant: "destructive", + }); + } + + console.error(error); + + return null; + } + }, + onTransaction, + }; +} diff --git a/packages/debugger/app/providers/ProtocolSelectorProvider.tsx b/packages/debugger/app/providers/ProtocolSelectorProvider.tsx new file mode 100644 index 000000000..1a1ccd82c --- /dev/null +++ b/packages/debugger/app/providers/ProtocolSelectorProvider.tsx @@ -0,0 +1,13 @@ +import { createContext, useContext } from "react"; + +const protocolSelectorContext = createContext({ + open() {}, +}); + +protocolSelectorContext.displayName = "ProtocolSelectorContext"; + +export function useProtocolSelector() { + return useContext(protocolSelectorContext); +} + +export const ProtocolSelectorProvider = protocolSelectorContext.Provider; From a6188b8462c742e810beb3e02d54304517c9cee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 14:36:06 +0100 Subject: [PATCH 10/88] feat: export signer instance types --- packages/render/src/identity/farcaster/index.ts | 6 +++++- .../render/src/identity/farcaster/use-farcaster-context.tsx | 2 ++ packages/render/src/identity/lens/index.ts | 3 ++- packages/render/src/identity/lens/use-lens-identity.tsx | 2 +- packages/render/src/identity/xmtp/index.ts | 6 +++++- packages/render/src/identity/xmtp/use-xmtp-identity.tsx | 2 +- packages/render/src/ui/frame.base.tsx | 2 +- 7 files changed, 17 insertions(+), 6 deletions(-) diff --git a/packages/render/src/identity/farcaster/index.ts b/packages/render/src/identity/farcaster/index.ts index c9cec3a42..b2eed0b66 100644 --- a/packages/render/src/identity/farcaster/index.ts +++ b/packages/render/src/identity/farcaster/index.ts @@ -1,4 +1,8 @@ -export { useFarcasterFrameContext } from "./use-farcaster-context"; +export { + type FarcasterFrameContext, + fallbackFrameContext, + useFarcasterFrameContext, +} from "./use-farcaster-context"; export type { FarcasterSignedKeyRequest, FarcasterSigner } from "./types"; export { type FarcasterSignerInstance, diff --git a/packages/render/src/identity/farcaster/use-farcaster-context.tsx b/packages/render/src/identity/farcaster/use-farcaster-context.tsx index f6abbb69c..8ac343b2c 100644 --- a/packages/render/src/identity/farcaster/use-farcaster-context.tsx +++ b/packages/render/src/identity/farcaster/use-farcaster-context.tsx @@ -1,6 +1,8 @@ import type { FarcasterFrameContext } from "../../farcaster/types"; import { createFrameContextHook } from "../create-frame-context-hook"; +export type { FarcasterFrameContext }; + export const useFarcasterFrameContext = createFrameContextHook({ storageKey: "farcasterFrameContext", diff --git a/packages/render/src/identity/lens/index.ts b/packages/render/src/identity/lens/index.ts index de1d08a97..2a05cc782 100644 --- a/packages/render/src/identity/lens/index.ts +++ b/packages/render/src/identity/lens/index.ts @@ -1,6 +1,7 @@ -export { useLensFrameContext } from "./use-lens-context"; +export { type LensFrameContext, useLensFrameContext } from "./use-lens-context"; export { type LensProfile, type LensSigner, + type LensSignerInstance, useLensIdentity, } from "./use-lens-identity"; diff --git a/packages/render/src/identity/lens/use-lens-identity.tsx b/packages/render/src/identity/lens/use-lens-identity.tsx index 8fe0a2642..732dda4a3 100644 --- a/packages/render/src/identity/lens/use-lens-identity.tsx +++ b/packages/render/src/identity/lens/use-lens-identity.tsx @@ -51,7 +51,7 @@ type LensFrameRequest = { }; }; -type LensSignerInstance = SignerStateInstance< +export type LensSignerInstance = SignerStateInstance< LensSigner, LensFrameRequest, LensFrameContext diff --git a/packages/render/src/identity/xmtp/index.ts b/packages/render/src/identity/xmtp/index.ts index c77e67e8f..814f5a04b 100644 --- a/packages/render/src/identity/xmtp/index.ts +++ b/packages/render/src/identity/xmtp/index.ts @@ -1,2 +1,6 @@ export { type XmtpFrameContext, useXmtpFrameContext } from "./use-xmtp-context"; -export { type XmtpSigner, useXmtpIdentity } from "./use-xmtp-identity"; +export { + type XmtpSigner, + type XmtpSignerInstance, + useXmtpIdentity, +} from "./use-xmtp-identity"; diff --git a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx index 91d29877b..042a8dd38 100644 --- a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx +++ b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx @@ -24,7 +24,7 @@ type XmtpStoredSigner = { keys: string; }; -type XmtpSignerInstance = SignerStateInstance< +export type XmtpSignerInstance = SignerStateInstance< XmtpSigner, FramePostPayload, XmtpFrameContext diff --git a/packages/render/src/ui/frame.base.tsx b/packages/render/src/ui/frame.base.tsx index a50edd7bf..bf59564a2 100644 --- a/packages/render/src/ui/frame.base.tsx +++ b/packages/render/src/ui/frame.base.tsx @@ -165,7 +165,7 @@ export function BaseFrameUI>({ const previousFrameStackItem = frameState.framesStack[frameState.framesStack.length - 1]; /** - * Frames v2 don' have previous frame as they consist purely of initial frame only + * Frames v2 don't have previous frame as they consist purely of initial frame only */ const previousFrame = previousFrameStackItem?.status === "done" && From 522e33e8293215869464428aff79c48f619b1654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 14:36:39 +0100 Subject: [PATCH 11/88] chore: add farcaster v2 to protocol selector --- .../app/components/action-debugger.tsx | 2 - .../app/components/cast-action-debugger.tsx | 6 +- .../app/components/frame-debugger.tsx | 58 +-- .../app/components/protocol-config-button.tsx | 196 ++++++---- packages/debugger/app/debugger-page.tsx | 354 +++++++++--------- packages/debugger/app/frames/route.ts | 4 +- .../app/providers/FrameContextProvider.tsx | 38 ++ 7 files changed, 363 insertions(+), 295 deletions(-) create mode 100644 packages/debugger/app/providers/FrameContextProvider.tsx diff --git a/packages/debugger/app/components/action-debugger.tsx b/packages/debugger/app/components/action-debugger.tsx index 2f5156308..478ffcd2a 100644 --- a/packages/debugger/app/components/action-debugger.tsx +++ b/packages/debugger/app/components/action-debugger.tsx @@ -10,7 +10,6 @@ import type { CastActionDefinitionResponse } from "../frames/route"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { ComposerActionDebugger } from "./composer-action-debugger"; import { CastActionDebugger } from "./cast-action-debugger"; -import { useProtocolSelector } from "../providers/ProtocolSelectorProvider"; type ActionDebuggerProps = { actionMetadataItem: CastActionDefinitionResponse; @@ -40,7 +39,6 @@ export const ActionDebugger = React.forwardRef< }, ref ) => { - const protocolSelector = useProtocolSelector(); const [activeTab, setActiveTab] = useState( "type" in actionMetadataItem.action && actionMetadataItem.action.type === "composer" diff --git a/packages/debugger/app/components/cast-action-debugger.tsx b/packages/debugger/app/components/cast-action-debugger.tsx index 1e23c7d2d..f3c7000ed 100644 --- a/packages/debugger/app/components/cast-action-debugger.tsx +++ b/packages/debugger/app/components/cast-action-debugger.tsx @@ -3,12 +3,13 @@ import type { CastActionDefinitionResponse } from "../frames/route"; import IconByName from "./octicons"; import { useToast } from "@/components/ui/use-toast"; import { ActionInfo } from "./action-info"; -import { defaultTheme, fallbackFrameContext } from "@frames.js/render"; +import { defaultTheme } from "@frames.js/render"; import { useCastAction } from "@frames.js/render/use-cast-action"; import { FrameDebugger } from "./frame-debugger"; import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; import { type Dispatch, type SetStateAction, useState } from "react"; import type { MockHubActionContext } from "../utils/mock-hub-utils"; +import { useFrameContext } from "../providers/FrameContextProvider"; type CastActionDebuggerProps = { actionMetadataItem: CastActionDefinitionResponse; @@ -28,6 +29,7 @@ export function CastActionDebugger({ const { toast } = useToast(); const farcasterIdentity = useFarcasterIdentity(); const [postUrl, setPostUrl] = useState(null); + const frameContext = useFrameContext(); const castAction = useCastAction({ ...(postUrl ? { @@ -38,7 +40,7 @@ export function CastActionDebugger({ enabled: false, postUrl: "", }), - castId: fallbackFrameContext.castId, + castId: frameContext.farcaster.castId, proxyUrl: "/frames", signer: farcasterIdentity, onInvalidResponse(response) { diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index 5ef278145..628a67457 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -10,12 +10,7 @@ import { } from "react"; import React from "react"; import { useFrame_unstable as useFrame } from "@frames.js/render/unstable-use-frame"; -import { - attribution, - CollapsedFrameUI, - defaultTheme, - fallbackFrameContext, -} from "@frames.js/render"; +import { attribution, CollapsedFrameUI, defaultTheme } from "@frames.js/render"; import { FrameImageNext } from "@frames.js/render/next"; import { BanIcon, @@ -45,21 +40,11 @@ import { FrameDebuggerRequestCardContent } from "./frame-debugger-request-card-c import { useSharedFrameEventHandlers } from "../hooks/useSharedFrameEventHandlers"; import { ProtocolConfiguration } from "./protocol-config-button"; import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; -import { - useXmtpFrameContext, - useXmtpIdentity, -} from "@frames.js/render/identity/xmtp"; -import { - useLensFrameContext, - useLensIdentity, -} from "@frames.js/render/identity/lens"; +import { useXmtpIdentity } from "@frames.js/render/identity/xmtp"; +import { useLensIdentity } from "@frames.js/render/identity/lens"; import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; -import { useFarcasterFrameContext } from "@frames.js/render/identity/farcaster"; -import { useAccount } from "wagmi"; -import { zeroAddress } from "viem"; import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; - -const anonymousFrameContext = {}; +import { useFrameContext } from "../providers/FrameContextProvider"; type FrameDebuggerProps = { url: string; @@ -91,30 +76,11 @@ export const FrameDebugger = React.forwardRef< }, ref ) => { - const account = useAccount(); const farcasterSignerState = useFarcasterIdentity(); const xmtpSignerState = useXmtpIdentity(); const lensSignerState = useLensIdentity(); const anonymousSignerState = useAnonymousIdentity(); - - const farcasterFrameContext = useFarcasterFrameContext({ - fallbackContext: fallbackFrameContext, - }); - - const xmtpFrameContext = useXmtpFrameContext({ - fallbackContext: { - conversationTopic: "test", - participantAccountAddresses: account.address - ? [account.address, zeroAddress] - : [zeroAddress], - }, - }); - - const lensFrameContext = useLensFrameContext({ - fallbackContext: { - pubId: "0x01-0x01", - }, - }); + const frameContext = useFrameContext(); const sharedFrameEventHandlers = useSharedFrameEventHandlers({ debuggerRef: null, @@ -142,19 +108,15 @@ export const FrameDebugger = React.forwardRef< // it creates copies of the signer which is bad because if the signer is internally // updated, we see only old value although the signer is updated. This is true only for public properties // probably getters will be better in this regard? - return farcasterSignerState.withContext( - farcasterFrameContext.frameContext - ); + return farcasterSignerState.withContext(frameContext.farcaster); case "farcaster_v2": - return farcasterSignerState.withContext( - farcasterFrameContext.frameContext - ); + return farcasterSignerState.withContext(frameContext.farcaster); case "xmtp": - return xmtpSignerState.withContext(xmtpFrameContext.frameContext); + return xmtpSignerState.withContext(frameContext.xmtp); case "lens": - return lensSignerState.withContext(lensFrameContext.frameContext); + return lensSignerState.withContext(frameContext.lens); case "anonymous": - return anonymousSignerState.withContext(anonymousFrameContext); + return anonymousSignerState.withContext(frameContext.anonymous); default: throw new Error(`Unknown protocol`); } diff --git a/packages/debugger/app/components/protocol-config-button.tsx b/packages/debugger/app/components/protocol-config-button.tsx index a393edd6d..5b7f8485c 100644 --- a/packages/debugger/app/components/protocol-config-button.tsx +++ b/packages/debugger/app/components/protocol-config-button.tsx @@ -10,18 +10,18 @@ import { isAddress } from "viem"; import FarcasterSignerWindow from "./farcaster-signer-config"; import { forwardRef, useMemo } from "react"; import { WithTooltip } from "./with-tooltip"; -import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; +import { type AnonymousSignerInstance } from "@frames.js/render/identity/anonymous"; import { + type FarcasterMultiSignerInstance, useFarcasterFrameContext, - useFarcasterMultiIdentity, } from "@frames.js/render/identity/farcaster"; import { useLensFrameContext, - useLensIdentity, + type LensSignerInstance, } from "@frames.js/render/identity/lens"; import { useXmtpFrameContext, - useXmtpIdentity, + type XmtpSignerInstance, } from "@frames.js/render/identity/xmtp"; export type ProtocolConfiguration = @@ -72,10 +72,10 @@ export const protocolConfigurationMap: Record = { type ProtocolConfigurationButtonProps = { onChange: (configuration: ProtocolConfiguration) => void; value: ProtocolConfiguration | null; - farcasterSignerState: ReturnType; - xmtpSignerState: ReturnType; - lensSignerState: ReturnType; - anonymousSignerState: ReturnType; + farcasterSignerState: FarcasterMultiSignerInstance; + xmtpSignerState: XmtpSignerInstance; + lensSignerState: LensSignerInstance; + anonymousSignerState: AnonymousSignerInstance; farcasterFrameContext: ReturnType; xmtpFrameContext: ReturnType; lensFrameContext: ReturnType; @@ -102,7 +102,10 @@ export const ProtocolConfigurationButton = forwardRef< const isSignerValid = useMemo(() => { let valid = false; - if (value?.protocol === "farcaster") { + if ( + value?.protocol === "farcaster" || + value?.protocol === "farcaster_v2" + ) { valid = !!farcasterSignerState.signer && farcasterSignerState.signer.status !== "pending_approval"; @@ -134,28 +137,18 @@ export const ProtocolConfigurationButton = forwardRef< Protocol and identity management

}>
- + - + onChange({ @@ -167,6 +160,7 @@ export const ProtocolConfigurationButton = forwardRef< None onChange({ @@ -178,6 +172,19 @@ export const ProtocolConfigurationButton = forwardRef< Farcaster + onChange({ + protocol: "farcaster_v2", + specification: "farcaster_v2", + }) + } + > + Farcaster v2 + + onChange({ protocol: "xmtp", specification: "openframes" }) @@ -186,6 +193,7 @@ export const ProtocolConfigurationButton = forwardRef< XMTP onChange({ protocol: "lens", specification: "openframes" }) @@ -194,61 +202,77 @@ export const ProtocolConfigurationButton = forwardRef< Lens - + -
-
Frame Context
-
Cast Hash
- { - farcasterFrameContext.setFrameContext({ - ...farcasterFrameContext.frameContext, - castId: { - fid: farcasterFrameContext.frameContext.castId.fid, - hash: e.target.value as unknown as `0x${string}`, - }, - }); - }} - /> -
Cast FID
- { - farcasterFrameContext.setFrameContext({ - ...farcasterFrameContext.frameContext, - castId: { - fid: parseInt(e.target.value), - hash: farcasterFrameContext.frameContext.castId.hash, - }, - }); - }} - /> - {/* Reset context button */} - -
+ /> + {value?.specification === "farcaster" && ( +
+
Frame Context
+
Cast Hash
+ { + farcasterFrameContext.setFrameContext({ + ...farcasterFrameContext.frameContext, + castId: { + fid: farcasterFrameContext.frameContext.castId.fid, + hash: e.target.value as unknown as `0x${string}`, + }, + }); + }} + /> +
Cast FID
+ { + farcasterFrameContext.setFrameContext({ + ...farcasterFrameContext.frameContext, + castId: { + fid: parseInt(e.target.value), + hash: farcasterFrameContext.frameContext.castId.hash, + }, + }); + }} + /> + {/* Reset context button */} + +
+ )} +
+ +
@@ -390,3 +414,31 @@ export const ProtocolConfigurationButton = forwardRef< ); ProtocolConfigurationButton.displayName = "ProtocolConfigurationButton"; + +function protocolToConfigurationToButtonLabel( + protocol: ProtocolConfiguration | null, + farcasterSigner: FarcasterMultiSignerInstance +) { + if (!protocol) { + return "Select a protocol"; + } + + const farcasterIdentity = + farcasterSigner.signer && + farcasterSigner.signer.status !== "pending_approval" + ? farcasterSigner.signer.fid + : "select identity"; + + switch (protocol.protocol) { + case "farcaster": + return `Farcaster (${farcasterIdentity})`; + case "farcaster_v2": + return `Farcaster v2 (${farcasterIdentity})`; + case "lens": + return `Lens ${protocol.specification}`; + case "xmtp": + return `XMTP ${protocol.specification}`; + default: + return protocol.protocol; + } +} diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index fee388f2d..3b7db3b2f 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -56,6 +56,7 @@ import { } from "@frames.js/render/identity/xmtp"; import { useFarcasterIdentity } from "./hooks/useFarcasterIdentity"; import { ProtocolSelectorProvider } from "./providers/ProtocolSelectorProvider"; +import { FrameContextProvider } from "./providers/FrameContextProvider"; const FALLBACK_URL = process.env.NEXT_PUBLIC_DEBUGGER_DEFAULT_URL || "http://localhost:3000"; @@ -230,11 +231,9 @@ export default function DebuggerPage({ const xmtpSignerState = useXmtpIdentity(); const lensSignerState = useLensIdentity(); const anonymousSignerState = useAnonymousIdentity(); - const farcasterFrameContext = useFarcasterFrameContext({ fallbackContext: fallbackFrameContext, }); - const xmtpFrameContext = useXmtpFrameContext({ fallbackContext: { conversationTopic: "test", @@ -243,7 +242,6 @@ export default function DebuggerPage({ : [zeroAddress], }, }); - const lensFrameContext = useLensFrameContext({ fallbackContext: { pubId: "0x01-0x01", @@ -258,196 +256,214 @@ export default function DebuggerPage({ }; }, []); + const frameContextValue = useMemo(() => { + return { + farcaster: farcasterFrameContext.frameContext, + xmtp: xmtpFrameContext.frameContext, + lens: lensFrameContext.frameContext, + anonymous: {}, + }; + }, [ + farcasterFrameContext.frameContext, + xmtpFrameContext.frameContext, + lensFrameContext.frameContext, + ]); + return ( - -
-
- - - - - - - - - - -
{ - e.preventDefault(); - - const newUrl = - new FormData(e.currentTarget).get("url")?.toString() || ""; - - if (!newUrl) { - toast({ - title: "Missing URL", - description: "Please provide a URL to debug", - variant: "destructive", - action: ( - { - urlInputRef.current?.focus(); - }} - type="button" - > - Fix - - ), - }); - return; - } - - try { - const parsedUrl = new URL(newUrl); - - if ( - parsedUrl.protocol !== "http:" && - parsedUrl.protocol !== "https:" - ) { - throw new Error("Invalid protocol"); - } - - if (!protocolConfiguration) { + + +
+
+ + + + + + + + + + + { + e.preventDefault(); + + const newUrl = + new FormData(e.currentTarget).get("url")?.toString() || ""; + + if (!newUrl) { toast({ - title: "Select Protocol", - description: "Please select a protocol to debug the URL", + title: "Missing URL", + description: "Please provide a URL to debug", variant: "destructive", action: ( { - selectProtocolButtonRef.current?.click(); + urlInputRef.current?.focus(); }} type="button" > - Select + Fix ), }); return; } - if (searchParams.url === parsedUrl.toString()) { - location.reload(); - } - - router.push( - `?url=${encodeURIComponent(parsedUrl.toString())}` - ); - } catch (e) { - toast({ - title: "Invalid URL", - description: - "URL must start with http:// or https:// and be in valid format", - variant: "destructive", - action: ( - { - urlInputRef.current?.focus(); - }} - type="button" - > - Fix - - ), - }); + try { + const parsedUrl = new URL(newUrl); + + if ( + parsedUrl.protocol !== "http:" && + parsedUrl.protocol !== "https:" + ) { + throw new Error("Invalid protocol"); + } + + if (!protocolConfiguration) { + toast({ + title: "Select Protocol", + description: + "Please select a protocol to debug the URL", + variant: "destructive", + action: ( + { + selectProtocolButtonRef.current?.click(); + }} + type="button" + > + Select + + ), + }); + return; + } + + if (searchParams.url === parsedUrl.toString()) { + location.reload(); + } + + router.push( + `?url=${encodeURIComponent(parsedUrl.toString())}` + ); + } catch (e) { + toast({ + title: "Invalid URL", + description: + "URL must start with http:// or https:// and be in valid format", + variant: "destructive", + action: ( + { + urlInputRef.current?.focus(); + }} + type="button" + > + Fix + + ), + }); - return; - } - }} - > - - - - - { - setProtocolConfiguration(spec); - }} - value={protocolConfiguration} - farcasterSignerState={farcasterSignerState} - xmtpSignerState={xmtpSignerState} - anonymousSignerState={anonymousSignerState} - farcasterFrameContext={farcasterFrameContext} - xmtpFrameContext={xmtpFrameContext} - ref={selectProtocolButtonRef} - lensFrameContext={lensFrameContext} - lensSignerState={lensSignerState} - > - -
- + + + + + { + setProtocolConfiguration(spec); + }} + value={protocolConfiguration} + farcasterSignerState={farcasterSignerState} + xmtpSignerState={xmtpSignerState} + anonymousSignerState={anonymousSignerState} + farcasterFrameContext={farcasterFrameContext} + xmtpFrameContext={xmtpFrameContext} + ref={selectProtocolButtonRef} + lensFrameContext={lensFrameContext} + lensSignerState={lensSignerState} + > + +
+ +
-
- {url ? ( - <> - {initialAction && ( -
- + {initialAction && ( +
+ +
+ )} + + {initialFrame && !!protocolConfiguration && ( + -
- )} - - {initialFrame && !!protocolConfiguration && ( - - )} - - ) : ( - examples + )} + + ) : ( + examples + )} +
+ {lensSignerState.showProfileSelector && ( + )} -
- {lensSignerState.showProfileSelector && ( - - )} - + + ); } diff --git a/packages/debugger/app/frames/route.ts b/packages/debugger/app/frames/route.ts index b6f987512..8c564fde5 100644 --- a/packages/debugger/app/frames/route.ts +++ b/packages/debugger/app/frames/route.ts @@ -21,7 +21,7 @@ export function isSpecificationValid( ): specification is SupportedParsingSpecification { return ( typeof specification === "string" && - ["farcaster", "openframes"].includes(specification) + ["farcaster", "farcaster_v2", "openframes"].includes(specification) ); } @@ -36,7 +36,7 @@ export async function GET(request: NextRequest): Promise { return Response.json({ message: "Invalid URL" }, { status: 400 }); } - if (!(specification === "farcaster" || specification === "openframes")) { + if (!isSpecificationValid(specification)) { return Response.json({ message: "Invalid specification" }, { status: 400 }); } diff --git a/packages/debugger/app/providers/FrameContextProvider.tsx b/packages/debugger/app/providers/FrameContextProvider.tsx new file mode 100644 index 000000000..7661e4e80 --- /dev/null +++ b/packages/debugger/app/providers/FrameContextProvider.tsx @@ -0,0 +1,38 @@ +import { + type FarcasterFrameContext, + fallbackFrameContext, +} from "@frames.js/render/identity/farcaster"; +import type { XmtpFrameContext } from "@frames.js/render/identity/xmtp"; +import type { LensFrameContext } from "@frames.js/render/identity/lens"; +import type { AnonymousFrameContext } from "@frames.js/render/identity/anonymous"; +import { createContext, useContext } from "react"; +import { zeroAddress } from "viem"; + +type FrameContextValue = { + anonymous: AnonymousFrameContext; + farcaster: FarcasterFrameContext; + lens: LensFrameContext; + xmtp: XmtpFrameContext; +}; + +const frameContext = createContext({ + anonymous: {}, + farcaster: fallbackFrameContext, + lens: { + pubId: "0x01-0x01", + }, + xmtp: { + conversationTopic: "test", + participantAccountAddresses: [zeroAddress], + }, +}); + +frameContext.displayName = "FrameContext"; + +const { Provider } = frameContext; + +export { Provider as FrameContextProvider }; + +export function useFrameContext() { + return useContext(frameContext); +} From 502f8b8f2e9e4b906abfde8187bc4ee791b5104e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 14:37:04 +0100 Subject: [PATCH 12/88] fix: make frame v2 parsing compatible with spec --- .../frames.js/src/frame-parsers/farcasterV2.ts | 18 +++++++++--------- packages/frames.js/src/types.ts | 10 +++------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index 31fbd8d1a..8da0b6502 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -197,10 +197,10 @@ function validateFrameButtonAction( "fc:frame", 'Key "type" in FrameEmbed.button.action must be a string' ); - } else if (parsedValue.type !== "launch") { + } else if (parsedValue.type !== "launch_frame") { reporter.error( "fc:frame", - 'Key "type" in FrameEmbed.button.action must be "launch"' + 'Key "type" in FrameEmbed.button.action must be "launch_frame"' ); } else { action.type = parsedValue.type; @@ -226,23 +226,23 @@ function validateFrameButtonAction( } // @todo optionaly validate splashImage dimensions and file size - if (!("splashImage" in parsedValue)) { + if (!("splashImageUrl" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "splashImage" in FrameEmbed.button.action' + 'Missing required key "splashImageUrl" in FrameEmbed.button.action' ); - } else if (typeof parsedValue.splashImage !== "string") { + } else if (typeof parsedValue.splashImageUrl !== "string") { reporter.error( "fc:frame", - 'Key "splashImage" in FrameEmbed.button.action must be a string' + 'Key "splashImageUrl" in FrameEmbed.button.action must be a string' ); - } else if (!URL.canParse(parsedValue.splashImage)) { + } else if (!URL.canParse(parsedValue.splashImageUrl)) { reporter.error( "fc:frame", - 'Key "splashImage" in FrameEmbed.button.action must be a valid URL' + 'Key "splashImageUrl" in FrameEmbed.button.action must be a valid URL' ); } else { - action.splashImage = parsedValue.splashImage; + action.splashImageUrl = parsedValue.splashImageUrl; } if (!("splashBackgroundColor" in parsedValue)) { diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index cfe735e5f..ddfd24757 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -43,17 +43,13 @@ export type FrameV2 = { title: string; action: { /** - * Must be 'launch' + * Must be 'launch_frame' */ - type: "launch"; + type: "launch_frame"; /** * App name */ name: string; - /** - * URL to App icon, must be 200x200px, less than 1MB - */ - icon: string; /** * App launch URL */ @@ -61,7 +57,7 @@ export type FrameV2 = { /** * URL to splash image, must 200x200px, less than 1MB */ - splashImage: string; + splashImageUrl: string; /** * Hex color code for splash background */ From 9acc960076e71f05ba70bc6dbea89f4f8cc2c467 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 15:48:52 +0100 Subject: [PATCH 13/88] fix: parsing frame button --- packages/frames.js/src/frame-parsers/farcasterV2.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index 8da0b6502..e7ee21620 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -85,7 +85,7 @@ export function parseFarcasterFrameV2( if (!("button" in parsedJSON)) { reporter.error("fc:frame", 'Missing required key "button" in FrameEmbed'); } else { - validateFrameButton(parsedJSON.button, reporter); + parsedFrame.button = parseFrameButton(parsedJSON.button, reporter); } if (reporter.hasErrors()) { @@ -105,7 +105,7 @@ export function parseFarcasterFrameV2( }; } -function validateFrameButton( +function parseFrameButton( parsedValue: unknown, reporter: Reporter ): ParsedFrameV2["button"] { @@ -143,13 +143,13 @@ function validateFrameButton( 'Missing required key "action" in FrameEmbed.button' ); } else { - validateFrameButtonAction(parsedValue.action, reporter); + parseFrameButtonAction(parsedValue.action, reporter); } return button; } -function validateFrameButtonAction( +function parseFrameButtonAction( parsedValue: unknown, reporter: Reporter ): NonNullable["action"] { From 92fbd7f1f0671a6b1b56f6c0a17cf9e6e4382abc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 15:49:36 +0100 Subject: [PATCH 14/88] feat: allow multi spec signer --- .../anonymous/use-anonymous-identity.tsx | 7 +- .../farcaster/use-farcaster-identity.tsx | 9 +- .../use-farcaster-multi-identity.tsx | 9 +- .../src/identity/lens/use-lens-identity.tsx | 7 +- .../src/identity/xmtp/use-xmtp-identity.tsx | 7 +- packages/render/src/types.ts | 16 +++- .../render/src/unstable-use-frame-state.ts | 85 ++++++++++++++++--- 7 files changed, 115 insertions(+), 25 deletions(-) diff --git a/packages/render/src/identity/anonymous/use-anonymous-identity.tsx b/packages/render/src/identity/anonymous/use-anonymous-identity.tsx index c1f1eb2eb..14fed933d 100644 --- a/packages/render/src/identity/anonymous/use-anonymous-identity.tsx +++ b/packages/render/src/identity/anonymous/use-anonymous-identity.tsx @@ -68,9 +68,12 @@ export function useAnonymousIdentity(): AnonymousSignerInstance { isLoadingSigner: false, logout, signFrameAction, - withContext(frameContext) { + withContext(frameContext, overrides) { return { - signerState: this, + signerState: { + ...this, + ...overrides, + }, frameContext, }; }, diff --git a/packages/render/src/identity/farcaster/use-farcaster-identity.tsx b/packages/render/src/identity/farcaster/use-farcaster-identity.tsx index 691c8fbc6..a853dfead 100644 --- a/packages/render/src/identity/farcaster/use-farcaster-identity.tsx +++ b/packages/render/src/identity/farcaster/use-farcaster-identity.tsx @@ -464,7 +464,7 @@ export function useFarcasterIdentity({ void isLoading; return { - specification: "farcaster", + specification: ["farcaster", "farcaster_v2"], get signer() { return farcasterUserRef.current; }, @@ -485,9 +485,12 @@ export function useFarcasterIdentity({ createSigner, logout, identityPoller, - withContext(frameContext) { + withContext(frameContext, overrides) { return { - signerState: this, + signerState: { + ...this, + ...overrides, + }, frameContext, }; }, diff --git a/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx b/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx index 87b87aaae..5e98bbb3a 100644 --- a/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx +++ b/packages/render/src/identity/farcaster/use-farcaster-multi-identity.tsx @@ -558,7 +558,7 @@ export function useFarcasterMultiIdentity({ void state.identities; return { - specification: "farcaster", + specification: ["farcaster", "farcaster_v2"], get signer() { return farcasterUserRef.current; }, @@ -584,9 +584,12 @@ export function useFarcasterMultiIdentity({ }, selectIdentity, identityPoller, - withContext(frameContext) { + withContext(frameContext, overrides) { return { - signerState: this, + signerState: { + ...this, + ...overrides, + }, frameContext, }; }, diff --git a/packages/render/src/identity/lens/use-lens-identity.tsx b/packages/render/src/identity/lens/use-lens-identity.tsx index 732dda4a3..b1806d930 100644 --- a/packages/render/src/identity/lens/use-lens-identity.tsx +++ b/packages/render/src/identity/lens/use-lens-identity.tsx @@ -404,9 +404,12 @@ export function useLensIdentity({ return availableProfilesRef.current; }, handleSelectProfile, - withContext(frameContext) { + withContext(frameContext, overrides) { return { - signerState: this, + signerState: { + ...this, + ...overrides, + }, frameContext, }; }, diff --git a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx index 042a8dd38..910f8022a 100644 --- a/packages/render/src/identity/xmtp/use-xmtp-identity.tsx +++ b/packages/render/src/identity/xmtp/use-xmtp-identity.tsx @@ -242,9 +242,12 @@ export function useXmtpIdentity({ }, onSignerlessFramePress, logout, - withContext(frameContext) { + withContext(frameContext, overrides) { return { - signerState: this, + signerState: { + ...this, + ...overrides, + }, frameContext, }; }, diff --git a/packages/render/src/types.ts b/packages/render/src/types.ts index 03ffefe03..76b32bdca 100644 --- a/packages/render/src/types.ts +++ b/packages/render/src/types.ts @@ -368,8 +368,13 @@ export interface SignerStateInstance< > { /** * For which specification is this signer required. + * + * If the value is an array it will take first valid specification if there is no valid specification + * it will return the first specification in array no matter the validity. */ - readonly specification: SupportedParsingSpecification; + readonly specification: + | SupportedParsingSpecification + | SupportedParsingSpecification[]; signer: TSignerStorageType | null; /** * True only if signer is approved or impersonating @@ -384,7 +389,14 @@ export interface SignerStateInstance< /** A function called when a frame button is clicked without a signer */ onSignerlessFramePress: () => Promise; logout: () => Promise; - withContext: (context: TFrameContextType) => { + withContext: ( + context: TFrameContextType, + overrides?: { + specification?: + | SupportedParsingSpecification + | SupportedParsingSpecification[]; + } + ) => { signerState: SignerStateInstance< TSignerStorageType, TFrameActionBodyType, diff --git a/packages/render/src/unstable-use-frame-state.ts b/packages/render/src/unstable-use-frame-state.ts index a170dcefa..642377980 100644 --- a/packages/render/src/unstable-use-frame-state.ts +++ b/packages/render/src/unstable-use-frame-state.ts @@ -1,6 +1,10 @@ import type { MutableRefObject } from "react"; import { useMemo, useReducer, useRef } from "react"; -import type { SupportedParsingSpecification } from "frames.js/frame-parsers"; +import type { + ParseFramesWithReportsResult, + ParseResultWithFrameworkDetails, + SupportedParsingSpecification, +} from "frames.js/frame-parsers"; import type { FrameContext, SignerStateInstance } from "./types"; import type { FrameReducerActions, @@ -14,6 +18,41 @@ import type { } from "./unstable-types"; import { useFreshRef } from "./hooks/use-fresh-ref"; +function resolveParseResultForSpecification( + parseResult: ParseFramesWithReportsResult, + specification: SupportedParsingSpecification | SupportedParsingSpecification[] +): ParseResultWithFrameworkDetails { + const specifications = Array.isArray(specification) + ? specification + : [specification]; + + if (specifications.length === 0) { + throw new Error("Signer does not have any specification defined"); + } + + // take first valid specification or return first one + let frameResult: ParseResultWithFrameworkDetails | undefined; + + for (const currentSpecification of specifications) { + // take first valid specification + if (parseResult[currentSpecification].status === "success") { + frameResult = parseResult[currentSpecification]; + break; + } + + // or take first one + if (!frameResult) { + frameResult = parseResult[currentSpecification]; + } + } + + if (!frameResult) { + throw new Error("No frame for the given specification"); + } + + return frameResult; +} + function createFramesStackReducer< TExtraPending = unknown, TExtraDone = unknown, @@ -98,7 +137,9 @@ function createFramesStackReducer< } let signerState: SignerStateInstance; - let specification: SupportedParsingSpecification; + let specification: + | SupportedParsingSpecification + | SupportedParsingSpecification[]; let frameContext: FrameContext; let homeframeUrl: string; let parseResult = action.parseResult; @@ -127,10 +168,15 @@ function createFramesStackReducer< } = state); } + const frameResult = resolveParseResultForSpecification( + action.parseResult, + specification + ); + state.stack[index] = { ...action.pendingItem, status: "done", - frameResult: action.parseResult[specification], + frameResult, extra: action.extra, }; @@ -140,7 +186,7 @@ function createFramesStackReducer< signerState, frameContext, homeframeUrl, - specification, + specification: frameResult.specification, type: "initialized", stack: state.stack.slice(), }; @@ -170,27 +216,40 @@ function createFramesStackReducer< parseResult: state.parseResult, }); + const frameResult = resolveParseResultForSpecification( + state.parseResult, + signerState.specification + ); + + const stackItem = state.stack[0]; + + if (stackItem?.status === "done") { + stackItem.frameResult = frameResult; + } + return { ...state, - stack: - !!state.stack[0] && state.stack.length > 0 ? [state.stack[0]] : [], + stack: stackItem ? [{ ...stackItem }] : [], type: "initialized", frameContext, signerState, - specification: signerState.specification, + specification: frameResult.specification, }; } case "RESET_INITIAL_FRAME": { const { frameContext = {}, signerState } = resolveSignerRef.current({ parseResult: action.parseResult, }); - const frameResult = action.parseResult[signerState.specification]; + const frameResult = resolveParseResultForSpecification( + action.parseResult, + signerState.specification + ); return { type: "initialized", signerState, frameContext, - specification: signerState.specification, + specification: frameResult.specification, homeframeUrl: action.homeframeUrl, parseResult: action.parseResult, stack: [ @@ -287,13 +346,17 @@ export function useFrameState< const { frameContext = {}, signerState } = resolveSpecification({ parseResult, }); - const frameResult = parseResult[signerState.specification]; + + const frameResult = resolveParseResultForSpecification( + parseResult, + signerState.specification + ); return { type: "initialized", frameContext, signerState, - specification: signerState.specification, + specification: frameResult.specification, homeframeUrl: frameUrl, parseResult, stack: [ From ceee7bbad25999514b3facb0d1522760e977f943 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 15:49:53 +0100 Subject: [PATCH 15/88] feat: allow debugger to lock specification --- packages/debugger/app/components/frame-debugger.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index 628a67457..5abd8c8a5 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -105,12 +105,10 @@ export const FrameDebugger = React.forwardRef< resolveSigner() { switch (protocol.protocol) { case "farcaster": - // it creates copies of the signer which is bad because if the signer is internally - // updated, we see only old value although the signer is updated. This is true only for public properties - // probably getters will be better in this regard? - return farcasterSignerState.withContext(frameContext.farcaster); case "farcaster_v2": - return farcasterSignerState.withContext(frameContext.farcaster); + return farcasterSignerState.withContext(frameContext.farcaster, { + specification: protocol.specification, + }); case "xmtp": return xmtpSignerState.withContext(frameContext.xmtp); case "lens": From 3f5f543d9ce880551ce25d3f3235c0db8ce38960 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 15:54:38 +0100 Subject: [PATCH 16/88] fix: launch button type --- packages/render/src/ui/frame.base.tsx | 9 +++++++-- packages/render/src/ui/types.ts | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/render/src/ui/frame.base.tsx b/packages/render/src/ui/frame.base.tsx index bf59564a2..a2541915d 100644 --- a/packages/render/src/ui/frame.base.tsx +++ b/packages/render/src/ui/frame.base.tsx @@ -94,7 +94,12 @@ export type BaseFrameUIProps> = { function defaultMessageHandler(): void {} // eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop -function defaultOnAppLaunchButtonPress(): void {} +function defaultOnAppLaunchButtonPress(): void { + // eslint-disable-next-line no-console -- provide at least some feedback to the user + console.info( + "@frames.js/render/ui/FrameUI.onAppLaunchButtonPress is not implemented" + ); +} function defaultErrorLogger(error: Error): void { // eslint-disable-next-line no-console -- provide at least some feedback to the user @@ -327,7 +332,7 @@ export function BaseFrameUI>({ { frameState: frameUiState, frameButton: { - action: "launch", + action: "launch_frame", label: frameUiState.frame.button.title, }, index: 0, diff --git a/packages/render/src/ui/types.ts b/packages/render/src/ui/types.ts index 56934dfd3..a238eb2c0 100644 --- a/packages/render/src/ui/types.ts +++ b/packages/render/src/ui/types.ts @@ -298,7 +298,7 @@ export type FrameImageProps = FrameUIStateProps & { ); export type FrameLaunchButton = { - action: "launch"; + action: "launch_frame"; label: string; }; From b2ee51da13d87011ac67a88f7d557b993d5dd753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 28 Nov 2024 16:14:39 +0100 Subject: [PATCH 17/88] chore: define handler for frame launching --- packages/render/src/ui/frame.base.tsx | 46 ++++++++++++++++------ packages/render/src/ui/types.ts | 23 +---------- packages/render/src/ui/utils.ts | 3 +- packages/render/src/unstable-types.ts | 43 ++++++++++++++++++++ packages/render/src/unstable-use-frame.tsx | 4 ++ 5 files changed, 85 insertions(+), 34 deletions(-) diff --git a/packages/render/src/ui/frame.base.tsx b/packages/render/src/ui/frame.base.tsx index a2541915d..59ae9765d 100644 --- a/packages/render/src/ui/frame.base.tsx +++ b/packages/render/src/ui/frame.base.tsx @@ -7,7 +7,7 @@ import { useState, } from "react"; import type { FrameState } from "../types"; -import type { UseFrameReturnValue } from "../unstable-types"; +import type { PartialFrameV2, UseFrameReturnValue } from "../unstable-types"; import { useFreshRef } from "../hooks/use-fresh-ref"; import type { FrameMessage, @@ -17,7 +17,6 @@ import type { RootContainerDimensions, RootContainerElement, FrameButtonProps, - PartialFrameV2, } from "./types"; import { getErrorMessageFromFramesStackItem, @@ -30,7 +29,7 @@ export type FrameUIComponents> = export type FrameUITheme> = Partial>; -export type AppLaunchButtonPressEvent = +export type FrameUILaunchFrameButtonPressEvent = | { status: "complete"; frame: FrameV2; @@ -42,6 +41,10 @@ export type AppLaunchButtonPressEvent = frameUIState: FrameUIState; }; +export type FrameUILaunchFrameButtonPressHandler = ( + event: FrameUILaunchFrameButtonPressEvent +) => void; + export type BaseFrameUIProps> = { frameState: | FrameState @@ -81,13 +84,13 @@ export type BaseFrameUIProps> = { * * Only Frames v2 support this feature. */ - onAppLaunchButtonPress?: (event: AppLaunchButtonPressEvent) => void; + onLaunchFrameButtonPress?: FrameUILaunchFrameButtonPressHandler; /** * Called when an error occurs in onAppLaunchButtonPress * * @defaultValue console.error() */ - onAppLaunchButtonPressError?: (error: Error) => void; + onLaunchFrameButtonPressError?: (error: Error) => void; }; // eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop @@ -113,19 +116,20 @@ export function BaseFrameUI>({ allowPartialFrame = false, enableImageDebugging = false, onError = defaultErrorLogger, - onAppLaunchButtonPressError = defaultErrorLogger, + onLaunchFrameButtonPressError = defaultErrorLogger, onMessage = defaultMessageHandler, createElement = reactCreateElement, - onAppLaunchButtonPress = defaultOnAppLaunchButtonPress, + onLaunchFrameButtonPress = defaultOnAppLaunchButtonPress, }: BaseFrameUIProps): JSX.Element | null { const [isImageLoading, setIsImageLoading] = useState(true); const { currentFrameStackItem } = frameState; const rootRef = useRef(null); const rootDimensionsRef = useRef(); const onErrorRef = useFreshRef(onError); - const onAppLaunchButtonPressErrorRef = useFreshRef( - onAppLaunchButtonPressError + const onLaunchFrameButtonPressErrorRef = useFreshRef( + onLaunchFrameButtonPressError ); + const onLaunchFrameButtonPressRef = useFreshRef(onLaunchFrameButtonPress); const onImageLoadEnd = useCallback(() => { setIsImageLoading(false); @@ -340,7 +344,27 @@ export function BaseFrameUI>({ onPress() { // we don't need to track dimensions here because this button does nothing to frame stack try { - onAppLaunchButtonPress( + if (!("onLaunchFrameButtonPress" in frameState)) { + throw new Error( + "onLaunchFrameButtonPress is not implemented, you are porbably using old useFrame hook" + ); + } + + // call onLaunchFrameButtonPress on useFrame() hook + // because that's where the core of the frame v2 message handling is implemented + frameState.onLaunchFrameButtonPress( + frameUiState.status === "complete" + ? { + status: "complete", + frame: frameUiState.frame, + } + : { + status: "partial", + frame: frameUiState.frame, + } + ); + + onLaunchFrameButtonPressRef.current( frameUiState.status === "complete" ? { status: "complete", @@ -354,7 +378,7 @@ export function BaseFrameUI>({ } ); } catch (e) { - onAppLaunchButtonPressErrorRef.current( + onLaunchFrameButtonPressErrorRef.current( e instanceof Error ? e : new Error(String(e)) ); } diff --git a/packages/render/src/ui/types.ts b/packages/render/src/ui/types.ts index a238eb2c0..74f6647a2 100644 --- a/packages/render/src/ui/types.ts +++ b/packages/render/src/ui/types.ts @@ -1,8 +1,7 @@ import type { Frame, FrameButton, FrameV2 } from "frames.js"; import type { createElement, ReactElement } from "react"; -import type { ParsedFrameV2 } from "frames.js/frame-parsers"; import type { FrameState } from "../types"; -import type { UseFrameReturnValue } from "../unstable-types"; +import type { PartialFrameV2, UseFrameReturnValue } from "../unstable-types"; /** * Allows to override styling props on all component of the Frame UI @@ -31,26 +30,6 @@ type RequiredFrameProperties = "image" | "buttons"; export type PartialFrame = Omit, RequiredFrameProperties> & Required>; -/** - * If partial frame rendering is enabled this is the shape of the frame - */ -export type PartialFrameV2 = Omit & { - imageUrl: NonNullable; - button: Omit, "action" | "title"> & { - action: Omit< - NonNullable["action"]>, - "url" | "title" - > & { - url: NonNullable< - NonNullable["action"]>["url"] - >; - }; - title: NonNullable< - NonNullable["title"]> - >; - }; -}; - type FrameUIStateLoading = { status: "loading"; id: number; diff --git a/packages/render/src/ui/utils.ts b/packages/render/src/ui/utils.ts index 9b5eeb0af..35cd0106d 100644 --- a/packages/render/src/ui/utils.ts +++ b/packages/render/src/ui/utils.ts @@ -6,11 +6,12 @@ import type { FrameStackRequestError, } from "../types"; import type { + PartialFrameV2, FramesStackItem as UnstableFramesStackItem, FrameStackMessage as UnstableFrameStackMessage, FrameStackRequestError as UnstableFrameStackRequestError, } from "../unstable-types"; -import type { PartialFrame, PartialFrameV2 } from "./types"; +import type { PartialFrame } from "./types"; type FrameResultFailure = Exclude; diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index a091951be..f44c59e0f 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -1,12 +1,14 @@ import type { FrameButtonLink, FrameButtonTx, + FrameV2, SupportedParsingSpecification, TransactionTargetResponse, TransactionTargetResponseSendTransaction, TransactionTargetResponseSignTypedDataV4, } from "frames.js"; import type { + ParsedFrameV2, ParseFramesWithReportsResult, ParseResultWithFrameworkDetails, } from "frames.js/frame-parsers"; @@ -62,6 +64,40 @@ export type ResolveSignerFunction = ( export type ResolveAddressFunction = () => Promise<`0x${string}` | null>; +/** + * If partial frame rendering is enabled this is the shape of the frame + */ +export type PartialFrameV2 = Omit & { + imageUrl: NonNullable; + button: Omit, "action" | "title"> & { + action: Omit< + NonNullable["action"]>, + "url" | "title" + > & { + url: NonNullable< + NonNullable["action"]>["url"] + >; + }; + title: NonNullable< + NonNullable["title"]> + >; + }; +}; + +export type LaunchFrameButtonPressEvent = + | { + status: "complete"; + frame: FrameV2; + } + | { + status: "partial"; + frame: PartialFrameV2; + }; + +export type LaunchFrameButtonPressFunction = ( + event: LaunchFrameButtonPressEvent +) => void; + export type UseFrameOptions< TExtraDataPending = unknown, TExtraDataDone = unknown, @@ -143,6 +179,12 @@ export type UseFrameOptions< * This function can be used to customize how the link button click is handled. */ onLinkButtonClick?: OnLinkButtonClickFunction; + /** + * Called when the frame button is pressed. + * + * Only valid for frames v2. + */ + onLaunchFrameButtonPress?: LaunchFrameButtonPressFunction; } & Partial< Pick< UseFetchFrameOptions, @@ -278,6 +320,7 @@ export type UseFrameReturnValue< readonly inputText: string; setInputText: (s: string) => void; onButtonPress: ButtonPressFunction>; + onLaunchFrameButtonPress: LaunchFrameButtonPressFunction; readonly homeframeUrl: string | null | undefined; /** * Resets the frame state to initial frame and resolves specification and signer again diff --git a/packages/render/src/unstable-use-frame.tsx b/packages/render/src/unstable-use-frame.tsx index 6dbc65cc4..54f3abe82 100644 --- a/packages/render/src/unstable-use-frame.tsx +++ b/packages/render/src/unstable-use-frame.tsx @@ -407,6 +407,8 @@ export function useFrame_unstable< [frameStateRef, fetchFrameRef, onErrorRef, resolveAddressRef] ); + const onLaunchFrameButtonPress = useCallback(() => {}, []); + const onButtonPress = useCallback( async function onButtonPress( currentFrame: Frame, @@ -532,6 +534,7 @@ export function useFrame_unstable< homeframeUrl, framesStack: stack, currentFrameStackItem: stack[0], + onLaunchFrameButtonPress, }; }, [ signerState, @@ -544,5 +547,6 @@ export function useFrame_unstable< fetchFrame, homeframeUrl, stack, + onLaunchFrameButtonPress, ]); } From a1504c0b4d501598fb640216d21b651286ae1628 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 29 Nov 2024 14:26:13 +0100 Subject: [PATCH 18/88] feat: add new hook to handle frames v2 app --- .../src/frame-parsers/farcasterV2.ts | 10 +- packages/frames.js/src/types.ts | 4 + packages/render/package.json | 32 ++- packages/render/src/ui/frame.base.tsx | 66 +----- packages/render/src/unstable-types.ts | 35 ++- packages/render/src/unstable-use-frame-app.ts | 200 ++++++++++++++++++ packages/render/src/unstable-use-frame.tsx | 24 ++- yarn.lock | 20 +- 8 files changed, 308 insertions(+), 83 deletions(-) create mode 100644 packages/render/src/unstable-use-frame-app.ts diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index e7ee21620..fc8c52778 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -67,6 +67,14 @@ export function parseFarcasterFrameV2( }; } + if (!("version" in parsedJSON)) { + reporter.error("fc:frame", 'Missing required key "version" in FrameEmbed'); + } else if (typeof parsedJSON.version !== "string") { + reporter.error("fc:frame", 'Key "version" in FrameEmbed must be a string'); + } else { + parsedFrame.version = parsedJSON.version; + } + if (!("imageUrl" in parsedJSON)) { reporter.error("fc:frame", 'Missing required key "imageUrl" in FrameEmbed'); } else if (typeof parsedJSON.imageUrl !== "string") { @@ -143,7 +151,7 @@ function parseFrameButton( 'Missing required key "action" in FrameEmbed.button' ); } else { - parseFrameButtonAction(parsedValue.action, reporter); + button.action = parseFrameButtonAction(parsedValue.action, reporter); } return button; diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index ddfd24757..d5e68956b 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -35,6 +35,10 @@ export type Frame = { }; export type FrameV2 = { + /** + * Version of frame v2 spec? + */ + version: string; /** * A URL to image with 1.91:1 aspect ratio smaller than 10MB. */ diff --git a/packages/render/package.json b/packages/render/package.json index 5ff2536df..05e5aea23 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -170,16 +170,6 @@ "default": "./dist/use-composer-action.cjs" } }, - "./unstable-use-frame-state": { - "import": { - "types": "./dist/unstable-use-frame-state.d.ts", - "default": "./dist/unstable-use-frame-state.js" - }, - "require": { - "types": "./dist/unstable-use-frame-state.d.cts", - "default": "./dist/unstable-use-frame-state.cjs" - } - }, "./unstable-use-fetch-frame": { "import": { "types": "./dist/unstable-use-fetch-frame.d.ts", @@ -190,6 +180,26 @@ "default": "./dist/unstable-use-fetch-frame.cjs" } }, + "./unstable-use-frame-app": { + "import": { + "types": "./dist/unstable-use-frame-app.d.ts", + "default": "./dist/unstable-use-frame-app.js" + }, + "require": { + "types": "./dist/unstable-use-frame-app.d.cts", + "default": "./dist/unstable-use-frame-app.cjs" + } + }, + "./unstable-use-frame-state": { + "import": { + "types": "./dist/unstable-use-frame-state.d.ts", + "default": "./dist/unstable-use-frame-state.js" + }, + "require": { + "types": "./dist/unstable-use-frame-state.d.cts", + "default": "./dist/unstable-use-frame-state.cjs" + } + }, "./unstable-use-frame": { "import": { "types": "./dist/unstable-use-frame.d.ts", @@ -313,8 +323,10 @@ "wagmi": "^2.9.10" }, "dependencies": { + "@farcaster/frame-sdk": "^0.0.3", "@farcaster/core": "^0.14.7", "@noble/ed25519": "^2.0.0", + "comlink": "^4.4.2", "frames.js": "^0.20.0", "zod": "^3.23.8" } diff --git a/packages/render/src/ui/frame.base.tsx b/packages/render/src/ui/frame.base.tsx index 59ae9765d..aa44435dd 100644 --- a/packages/render/src/ui/frame.base.tsx +++ b/packages/render/src/ui/frame.base.tsx @@ -1,4 +1,4 @@ -import type { Frame, FrameV2 } from "frames.js"; +import type { Frame } from "frames.js"; import { createElement as reactCreateElement, useCallback, @@ -7,7 +7,7 @@ import { useState, } from "react"; import type { FrameState } from "../types"; -import type { PartialFrameV2, UseFrameReturnValue } from "../unstable-types"; +import type { UseFrameReturnValue } from "../unstable-types"; import { useFreshRef } from "../hooks/use-fresh-ref"; import type { FrameMessage, @@ -29,22 +29,6 @@ export type FrameUIComponents> = export type FrameUITheme> = Partial>; -export type FrameUILaunchFrameButtonPressEvent = - | { - status: "complete"; - frame: FrameV2; - frameUIState: FrameUIState; - } - | { - status: "partial"; - frame: PartialFrameV2; - frameUIState: FrameUIState; - }; - -export type FrameUILaunchFrameButtonPressHandler = ( - event: FrameUILaunchFrameButtonPressEvent -) => void; - export type BaseFrameUIProps> = { frameState: | FrameState @@ -79,31 +63,11 @@ export type BaseFrameUIProps> = { * @defaultValue React.createElement */ createElement?: typeof reactCreateElement; - /** - * Called when user presses launch button on v2 frame. - * - * Only Frames v2 support this feature. - */ - onLaunchFrameButtonPress?: FrameUILaunchFrameButtonPressHandler; - /** - * Called when an error occurs in onAppLaunchButtonPress - * - * @defaultValue console.error() - */ - onLaunchFrameButtonPressError?: (error: Error) => void; }; // eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop function defaultMessageHandler(): void {} -// eslint-disable-next-line @typescript-eslint/no-empty-function -- this is noop -function defaultOnAppLaunchButtonPress(): void { - // eslint-disable-next-line no-console -- provide at least some feedback to the user - console.info( - "@frames.js/render/ui/FrameUI.onAppLaunchButtonPress is not implemented" - ); -} - function defaultErrorLogger(error: Error): void { // eslint-disable-next-line no-console -- provide at least some feedback to the user console.error(error); @@ -116,20 +80,14 @@ export function BaseFrameUI>({ allowPartialFrame = false, enableImageDebugging = false, onError = defaultErrorLogger, - onLaunchFrameButtonPressError = defaultErrorLogger, onMessage = defaultMessageHandler, createElement = reactCreateElement, - onLaunchFrameButtonPress = defaultOnAppLaunchButtonPress, }: BaseFrameUIProps): JSX.Element | null { const [isImageLoading, setIsImageLoading] = useState(true); const { currentFrameStackItem } = frameState; const rootRef = useRef(null); const rootDimensionsRef = useRef(); const onErrorRef = useFreshRef(onError); - const onLaunchFrameButtonPressErrorRef = useFreshRef( - onLaunchFrameButtonPressError - ); - const onLaunchFrameButtonPressRef = useFreshRef(onLaunchFrameButtonPress); const onImageLoadEnd = useCallback(() => { setIsImageLoading(false); @@ -350,8 +308,6 @@ export function BaseFrameUI>({ ); } - // call onLaunchFrameButtonPress on useFrame() hook - // because that's where the core of the frame v2 message handling is implemented frameState.onLaunchFrameButtonPress( frameUiState.status === "complete" ? { @@ -363,24 +319,8 @@ export function BaseFrameUI>({ frame: frameUiState.frame, } ); - - onLaunchFrameButtonPressRef.current( - frameUiState.status === "complete" - ? { - status: "complete", - frame: frameUiState.frame, - frameUIState: frameUiState, - } - : { - status: "partial", - frame: frameUiState.frame, - frameUIState: frameUiState, - } - ); } catch (e) { - onLaunchFrameButtonPressErrorRef.current( - e instanceof Error ? e : new Error(String(e)) - ); + onErrorRef.current(e instanceof Error ? e : new Error(String(e))); } }, }, diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index f44c59e0f..ef70cfb1d 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -98,6 +98,18 @@ export type LaunchFrameButtonPressFunction = ( event: LaunchFrameButtonPressEvent ) => void; +export type LaunchFrameOpenedEvent = + | { + status: "complete"; + frame: FrameV2; + } + | { + status: "partial"; + frame: PartialFrameV2; + }; + +export type FrameCloseFunction = () => void; + export type UseFrameOptions< TExtraDataPending = unknown, TExtraDataDone = unknown, @@ -180,11 +192,17 @@ export type UseFrameOptions< */ onLinkButtonClick?: OnLinkButtonClickFunction; /** - * Called when the frame button is pressed. + * This function is called when opening a Frames v2 app is requested * - * Only valid for frames v2. + * Only for frames v2 */ - onLaunchFrameButtonPress?: LaunchFrameButtonPressFunction; + onLaunchFrameButtonPressed?: LaunchFrameButtonPressFunction; + /** + * This function is called when lauched frame is closed. + * + * Only for frames v2 + */ + onLaunchedFrameClosed?: FrameCloseFunction; } & Partial< Pick< UseFetchFrameOptions, @@ -320,7 +338,18 @@ export type UseFrameReturnValue< readonly inputText: string; setInputText: (s: string) => void; onButtonPress: ButtonPressFunction>; + /** + * Called by UI when the launch frame button is pressed. + * + * Only for frames v2 + */ onLaunchFrameButtonPress: LaunchFrameButtonPressFunction; + /** + * Called by UI when the launched frame is closed. + * + * Only for frames v2 + */ + onLaunchedFrameClose: FrameCloseFunction; readonly homeframeUrl: string | null | undefined; /** * Resets the frame state to initial frame and resolves specification and signer again diff --git a/packages/render/src/unstable-use-frame-app.ts b/packages/render/src/unstable-use-frame-app.ts new file mode 100644 index 000000000..3b59d706a --- /dev/null +++ b/packages/render/src/unstable-use-frame-app.ts @@ -0,0 +1,200 @@ +import type { FrameV2 } from "frames.js"; +import { expose, windowEndpoint, type Endpoint } from "comlink"; +import { useCallback, useMemo } from "react"; +import type { FrameHost, SetPrimaryButton } from "@farcaster/frame-sdk"; +import type { UseWalletClientReturnType } from "wagmi"; +import { useFreshRef } from "./hooks/use-fresh-ref"; +import type { FarcasterSignerState } from "./farcaster"; +import type { FarcasterSigner } from "./identity/farcaster"; + +function defaultOnSignerNotApproved(): void { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.error( + "@frames.js/render/unstable-use-frame-app", + "Signer not approved" + ); +} + +export type FramePrimaryButton = Parameters[0]; + +type UseFrameAppOptions = { + /** + * Wallet client from wagmi's useWalletClient() hook + */ + walletClient: UseWalletClientReturnType; + /** + * Obtained from useFrame() onLaunchFrameButtonPressed() callback + */ + frame: FrameV2; + /** + * Farcaster signer state. Must be already approved otherwise it will call onError + * and getting context in app will be rejected + */ + farcasterSigner: FarcasterSignerState; + /** + * Called when app calls `ready` method. + */ + onReady?: () => void; + /** + * Called when app calls `close` method. + */ + onClose?: () => void; + /** + * Called when app calls `openUrl` method. + */ + onOpenUrl?: (url: string) => void; + /** + * Called when provided signer is not approved. + */ + onSignerNotApproved?: () => void; + /** + * Called when app calls `setPrimaryButton` method. + */ + onPrimaryButtonSet?: SetPrimaryButton; +}; + +type RegisterEndpointFunction = (endpoint: Endpoint) => void; + +type UseFrameAppReturn = { + /** + * Necessary to call with target endpoint to expose API to the frame. + */ + registerEndpoint: RegisterEndpointFunction; +}; + +/** + * This hook is used to handle frames v2 apps. + */ +export function useFrameApp({ + walletClient: client, + farcasterSigner, + frame, + onClose, + onOpenUrl, + onPrimaryButtonSet, + onReady, + onSignerNotApproved = defaultOnSignerNotApproved, +}: UseFrameAppOptions): UseFrameAppReturn { + const clientRef = useFreshRef(client); + const readyRef = useFreshRef(onReady); + const closeRef = useFreshRef(onClose); + const onOpenUrlRef = useFreshRef(onOpenUrl); + const onSignerNotApprovedRef = useFreshRef(onSignerNotApproved); + const onPrimaryButtonSetRef = useFreshRef(onPrimaryButtonSet); + const farcasterSignerRef = useFreshRef(farcasterSigner); + + // @todo solve expose isolation per endpoint because it's not possible to clean up the exposed API at the moment unless the target releases its proxy + // @see https://github.com/GoogleChromeLabs/comlink/issues/674 + // Perhaps this hook should be global and only once per whole app for now? + const registerEndpoint = useCallback( + (endpoint) => { + const signer = farcasterSignerRef.current.signer; + + if (signer?.status !== "approved") { + onSignerNotApprovedRef.current(); + return; + } + + expose( + { + close() { + const handler = closeRef.current; + + if (!handler) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + '@frames.js/render/unstable-use-frame-app: "close" called but no handler provided' + ); + } else { + handler(); + } + }, + get context() { + return { user: { fid: signer.fid } }; + }, + openUrl(url) { + const handler = onOpenUrlRef.current; + + if (!handler) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + '@frames.js/render/unstable-use-frame-app: "openUrl" called but no handler provided' + ); + } else { + handler(url); + } + }, + ready() { + const handler = readyRef.current; + + if (!handler) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + '@frames.js/render/unstable-use-frame-app: "ready" called but no handler provided' + ); + } else { + handler(); + } + }, + setPrimaryButton(options) { + const handler = onPrimaryButtonSetRef.current; + + if (!handler) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.warn( + '@frames.js/render/unstable-use-frame-app: "setPrimaryButton" called but no handler provided' + ); + } else { + handler(options); + } + }, + // @ts-expect-error -- types are mismatched + async ethProviderRequest(...args) { + // @ts-expect-error -- types are mismatched + return clientRef.current.data?.request(...args); + }, + } satisfies FrameHost, + endpoint, + [new URL(frame.button.action.url).origin] + ); + }, + [ + clientRef, + closeRef, + farcasterSignerRef, + frame.button.action.url, + onOpenUrlRef, + onPrimaryButtonSetRef, + onSignerNotApprovedRef, + readyRef, + ] + ); + + return useMemo(() => { + return { registerEndpoint }; + }, [registerEndpoint]); +} + +type UseFrameAppInIframeReturn = { + onLoad: (event: React.SyntheticEvent) => void; +}; + +export function useFrameAppInIframe( + options: UseFrameAppOptions +): UseFrameAppInIframeReturn { + const frameApp = useFrameApp(options); + + return useMemo(() => { + return { + onLoad(event) { + if (!event.currentTarget.contentWindow) { + return; + } + + frameApp.registerEndpoint( + windowEndpoint(event.currentTarget.contentWindow) + ); + }, + }; + }, [frameApp]); +} diff --git a/packages/render/src/unstable-use-frame.tsx b/packages/render/src/unstable-use-frame.tsx index 54f3abe82..c82c8a78c 100644 --- a/packages/render/src/unstable-use-frame.tsx +++ b/packages/render/src/unstable-use-frame.tsx @@ -11,7 +11,11 @@ import type { TransactionTargetResponse, } from "frames.js"; import type { OnMintArgs, OnTransactionArgs, OnSignatureArgs } from "./types"; -import type { UseFrameOptions, UseFrameReturnValue } from "./unstable-types"; +import type { + LaunchFrameButtonPressEvent, + UseFrameOptions, + UseFrameReturnValue, +} from "./unstable-types"; import { useFrameState } from "./unstable-use-frame-state"; import { useFetchFrame } from "./unstable-use-fetch-frame"; import { useFreshRef } from "./hooks/use-fresh-ref"; @@ -174,6 +178,8 @@ export function useFrame_unstable< onLinkButtonClick = handleLinkButtonClickFallback, onRedirect = handleRedirectFallback, fetchFn = defaultFetchFunction, + onLaunchFrameButtonPressed, + onLaunchedFrameClosed, onTransactionDataError, onTransactionDataStart, onTransactionDataSuccess, @@ -407,7 +413,19 @@ export function useFrame_unstable< [frameStateRef, fetchFrameRef, onErrorRef, resolveAddressRef] ); - const onLaunchFrameButtonPress = useCallback(() => {}, []); + const onLaunchFrameButtonPressRef = useFreshRef(onLaunchFrameButtonPressed); + const onLaunchedFrameCloseRef = useFreshRef(onLaunchedFrameClosed); + + const onLaunchFrameButtonPress = useCallback( + (event: LaunchFrameButtonPressEvent) => { + onLaunchFrameButtonPressRef.current?.(event); + }, + [onLaunchFrameButtonPressRef] + ); + + const onLaunchedFrameClose = useCallback(() => { + onLaunchedFrameCloseRef.current?.(); + }, [onLaunchedFrameCloseRef]); const onButtonPress = useCallback( async function onButtonPress( @@ -535,6 +553,7 @@ export function useFrame_unstable< framesStack: stack, currentFrameStackItem: stack[0], onLaunchFrameButtonPress, + onLaunchedFrameClose, }; }, [ signerState, @@ -548,5 +567,6 @@ export function useFrame_unstable< homeframeUrl, stack, onLaunchFrameButtonPress, + onLaunchedFrameClose, ]); } diff --git a/yarn.lock b/yarn.lock index 31372253c..636b4b9ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2419,10 +2419,10 @@ dependencies: ox "^0.1.6" -"@farcaster/frame-sdk@^0.0.2": - version "0.0.2" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.2.tgz#a052898d058cb3410bdbf02491727cca59dc6449" - integrity sha512-d+7w662rfBvbTFpaRHtP8U3zjc7GNVEqsu1/2jrci/JGwWCSFwFUmH0V/kINzsYBcrn1hT57c5is8cshrYy83A== +"@farcaster/frame-sdk@^0.0.3": + version "0.0.3" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.3.tgz#72159b447736ea0842c3bd08d0506f36eb787cd8" + integrity sha512-p9S7SSbIhOL/o9VIkbdlGqG7ooTopECm1PzxsZx/5CF+rC7xaqQQcwG4YgqIOg030tq1GprXFSChKCe7VJP3OA== dependencies: "@farcaster/frame-core" "^0.0.4" comlink "^4.4.2" @@ -4019,6 +4019,11 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-compose-refs@1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74" + integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw== + "@radix-ui/react-context@1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.0.0.tgz#f38e30c5859a9fb5e9aa9a9da452ee3ed9e0aee0" @@ -4327,6 +4332,13 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-slot@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84" + integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw== + dependencies: + "@radix-ui/react-compose-refs" "1.1.0" + "@radix-ui/react-switch@^1.0.3": version "1.0.3" resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" From 43801264432efcaae4e7025004fc8e5c3fe3a659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 29 Nov 2024 14:28:15 +0100 Subject: [PATCH 19/88] feat: add app frame dialog --- .../app/components/frame-app-dialog.tsx | 126 ++++ .../app/components/frame-debugger.tsx | 620 ++++++++++-------- 2 files changed, 461 insertions(+), 285 deletions(-) create mode 100644 packages/debugger/app/components/frame-app-dialog.tsx diff --git a/packages/debugger/app/components/frame-app-dialog.tsx b/packages/debugger/app/components/frame-app-dialog.tsx new file mode 100644 index 000000000..80b8087a3 --- /dev/null +++ b/packages/debugger/app/components/frame-app-dialog.tsx @@ -0,0 +1,126 @@ +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { useRef, useState } from "react"; +import { + useFrameAppInIframe, + type FramePrimaryButton, +} from "@frames.js/render/unstable-use-frame-app"; +import type { LaunchFrameOpenedEvent } from "@frames.js/render/unstable-types"; +import Image from "next/image"; +import { cn } from "@/lib/utils"; +import type { FarcasterMultiSignerInstance } from "@frames.js/render/identity/farcaster"; +import { Loader2Icon } from "lucide-react"; +import { useWalletClient } from "wagmi"; +import { Button } from "@/components/ui/button"; + +type FrameAppDialogProps = { + farcasterSigner: FarcasterMultiSignerInstance; + frameState: Extract; + onClose: () => void; +}; + +/** + * Frames v2 dialog web view + */ +export function FrameAppDialog({ + farcasterSigner, + frameState, + onClose, +}: FrameAppDialogProps) { + const walletClient = useWalletClient(); + const [isReady, setIsReady] = useState(false); + const [primaryButton, setPrimaryButton] = useState( + null + ); + const frameApp = useFrameAppInIframe({ + walletClient, + farcasterSigner, + frame: frameState.frame, + onReady() { + setIsReady(true); + }, + onClose, + onOpenUrl(url) { + window.open(url, "_blank"); + }, + onPrimaryButtonSet: setPrimaryButton, + }); + const iframeRef = useRef(null); + const { name, url, splashImageUrl, splashBackgroundColor } = + frameState.frame.button.action; + + return ( + { + if (!isOpen) { + onClose(); + } + }} + > + + + {frameState.frame.button.action.name} + +
+ {!isReady && ( +
+
+ {`${name} +
+ +
+
+
+ )} + +
+ {primaryButton && !primaryButton.hidden && ( + + + + )} +
+
+ ); +} diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index 5abd8c8a5..f8e0c22fc 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -45,6 +45,8 @@ import { useLensIdentity } from "@frames.js/render/identity/lens"; import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; import { useFrameContext } from "../providers/FrameContextProvider"; +import { LaunchFrameOpenedEvent } from "@frames.js/render/unstable-types"; +import { FrameAppDialog } from "./frame-app-dialog"; type FrameDebuggerProps = { url: string; @@ -76,12 +78,18 @@ export const FrameDebugger = React.forwardRef< }, ref ) => { + const { toast } = useToast(); const farcasterSignerState = useFarcasterIdentity(); const xmtpSignerState = useXmtpIdentity(); const lensSignerState = useLensIdentity(); const anonymousSignerState = useAnonymousIdentity(); const frameContext = useFrameContext(); + const [launchedFrame, setLaunchedFrame] = useState | null>(null); + const sharedFrameEventHandlers = useSharedFrameEventHandlers({ debuggerRef: null, }); @@ -144,8 +152,33 @@ export const FrameDebugger = React.forwardRef< ), }); }, + onLaunchFrameButtonPressed(event) { + if (event.status === "partial") { + toast({ + title: "Partial frame loaded", + description: + "The frame is partially invalid, please fix the errors.", + variant: "destructive", + action: ( + { + wantsToScrollConsoleToBottomRef.current = true; + setActiveTab("console"); + }} + > + Show console + + ), + }); + } else { + setLaunchedFrame(event); + } + }, + onLaunchedFrameClosed() { + setLaunchedFrame(null); + }, }); - const { toast } = useToast(); const debuggerConsoleTabRef = useRef(null); const [activeTab, setActiveTab] = useState("diagnostics"); const router = useRouter(); @@ -211,304 +244,321 @@ export const FrameDebugger = React.forwardRef< !!currentFrameStackItem.frameResult.framesDebugInfo?.image; return ( -
-
-
- Fetch home frame

}> - -
- Clear history and fetch home frame

}> - -
- Reload current frame

}> - -
+ <> +
+
+
+ Fetch home frame

}> + +
+ Clear history and fetch home frame

}> + +
+ Reload current frame

}> + +
+
+ + + + + + {protocol.specification === "farcaster" && + mockHubContext && + setMockHubContext && ( + + )} + + + Debug + + +
+ +
+
+
+
- - - - - - {protocol.specification === "farcaster" && - mockHubContext && - setMockHubContext && ( - +
+ - )} - - - Debug - - -
- -
-
-
- -
-
-
- -
{url}
+
{url}
- {!isLoading && ( - <> - {currentFrameStackItem?.request.method === "GET" && ( -
-

Preview

-
- + {!isLoading && protocol.specification !== "farcaster_v2" && ( + <> + {currentFrameStackItem?.request.method === "GET" && ( +
+

Preview

+
+ +
+ )} +
+ {currentFrameStackItem?.status === "done" && + (currentFrameStackItem.frameResult.specification === + "farcaster" || + currentFrameStackItem.frameResult.specification === + "openframes") && + currentFrameStackItem.frameResult.frame.buttons + ?.filter( + (button) => + button.target?.startsWith( + "https://warpcast.com/~/add-cast-action" + ) || + button.target?.startsWith( + "https://warpcast.com/~/composer-action" + ) + ) + .map((button) => { + // Link to debug target + return ( + + ); + })}
- )} -
- {currentFrameStackItem?.status === "done" && - (currentFrameStackItem.frameResult.specification === - "farcaster" || - currentFrameStackItem.frameResult.specification === - "openframes") && - currentFrameStackItem.frameResult.frame.buttons - ?.filter( - (button) => - button.target?.startsWith( - "https://warpcast.com/~/add-cast-action" - ) || - button.target?.startsWith( - "https://warpcast.com/~/composer-action" - ) - ) - .map((button) => { - // Link to debug target - return ( + + )} +
+
+
+ {currentFrameStackItem ? ( + + + setActiveTab(value as TabValues)} + className="grid grid-rows-[auto_1fr] w-full h-full" + > + + Diagnostics + Console + Request + Meta Tags + + + + + + { + if ( + wantsToScrollConsoleToBottomRef.current && + debuggerConsoleTabRef.current + ) { + wantsToScrollConsoleToBottomRef.current = false; + debuggerConsoleTabRef.current.scrollTo( + 0, + element.scrollHeight + ); + } + }} + /> + + + + + + {currentFrameStackItem.status === "done" ? ( +
+ html tags +
-                            
-                            
-                              Debug{" "}
-                              {button.label}
-                            
-                          
-                        );
-                      })}
-                
- - )} + {currentFrameStackItem.frameResult.specification === + "farcaster_v2" + ? getFrameV2HtmlHead( + currentFrameStackItem.frameResult.frame + ) + : getFrameHtmlHead( + "sourceFrame" in + currentFrameStackItem.request && + currentFrameStackItem.request.sourceFrame + ? currentFrameStackItem.request.sourceFrame + : currentFrameStackItem.frameResult.frame + ) + .split(" !!t) + // hacky... + .flatMap((el, i) => [ + {`, +
, + ])} + +
+ ) : null} + + + + + ) : null}
-
- {currentFrameStackItem ? ( - - - setActiveTab(value as TabValues)} - className="grid grid-rows-[auto_1fr] w-full h-full" - > - - Diagnostics - Console - Request - Meta Tags - - - - - - { - if ( - wantsToScrollConsoleToBottomRef.current && - debuggerConsoleTabRef.current - ) { - wantsToScrollConsoleToBottomRef.current = false; - debuggerConsoleTabRef.current.scrollTo( - 0, - element.scrollHeight - ); - } - }} - /> - - - - - - {currentFrameStackItem.status === "done" ? ( -
- html tags - -
-                          {currentFrameStackItem.frameResult.specification ===
-                          "farcaster_v2"
-                            ? getFrameV2HtmlHead(
-                                currentFrameStackItem.frameResult.frame
-                              )
-                            : getFrameHtmlHead(
-                                "sourceFrame" in
-                                  currentFrameStackItem.request &&
-                                  currentFrameStackItem.request.sourceFrame
-                                  ? currentFrameStackItem.request.sourceFrame
-                                  : currentFrameStackItem.frameResult.frame
-                              )
-                                .split(" !!t)
-                                // hacky...
-                                .flatMap((el, i) => [
-                                  {`,
-                                  
, - ])} -
-
- ) : null} -
-
-
-
- ) : null} -
-
+ {launchedFrame && ( + frameState.onLaunchedFrameClose()} + /> + )} + ); } ); From 63ba230be8c9be63b4950e36474aa960cc9b01e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 29 Nov 2024 14:28:37 +0100 Subject: [PATCH 20/88] chore: basic frames 2 example --- .../@/components/ui/button.tsx | 56 ++++++++++++++++++ .../next-frames-v2-starter/@/lib/utils.ts | 6 ++ .../app/.well-known/frames.json/route.ts | 15 +++-- templates/next-frames-v2-starter/app/App.tsx | 37 ++++++++++-- .../app/WagmiProvider.tsx | 1 + .../next-frames-v2-starter/app/globals.css | 38 ++++++------ .../next-frames-v2-starter/app/layout.tsx | 27 ++------- templates/next-frames-v2-starter/app/page.tsx | 4 +- .../next-frames-v2-starter/app/ui/Button.tsx | 11 ++-- .../next-frames-v2-starter/components.json | 17 ++++++ templates/next-frames-v2-starter/package.json | 4 +- .../next-frames-v2-starter/public/frame.png | Bin 0 -> 536945 bytes 12 files changed, 157 insertions(+), 59 deletions(-) create mode 100644 templates/next-frames-v2-starter/@/components/ui/button.tsx create mode 100644 templates/next-frames-v2-starter/@/lib/utils.ts create mode 100644 templates/next-frames-v2-starter/components.json create mode 100644 templates/next-frames-v2-starter/public/frame.png diff --git a/templates/next-frames-v2-starter/@/components/ui/button.tsx b/templates/next-frames-v2-starter/@/components/ui/button.tsx new file mode 100644 index 000000000..36496a287 --- /dev/null +++ b/templates/next-frames-v2-starter/@/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/templates/next-frames-v2-starter/@/lib/utils.ts b/templates/next-frames-v2-starter/@/lib/utils.ts new file mode 100644 index 000000000..d084ccade --- /dev/null +++ b/templates/next-frames-v2-starter/@/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx" +import { twMerge } from "tailwind-merge" + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)) +} diff --git a/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts b/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts index 1e140d1a4..8052117f2 100644 --- a/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts +++ b/templates/next-frames-v2-starter/app/.well-known/frames.json/route.ts @@ -2,16 +2,19 @@ export async function GET() { const appUrl = process.env.APP_URL; const config = { - config: { + // @todo replace with our own association + accountAssociation: { + header: "", + payload: "", + signature: "", + }, + frame: { version: "0.0.0", name: "Frames v2 Demo", - icon: new URL("/icon.png", appUrl).toString(), - splashImage: new URL("/splash.png", appUrl).toString(), + iconUrl: `${appUrl}/icon.png`, + splashImageUrl: `${appUrl}/splash.png`, splashBackgroundColor: "#f7f7f7", homeUrl: appUrl, - fid: 0, - key: "", - signature: "", }, }; diff --git a/templates/next-frames-v2-starter/app/App.tsx b/templates/next-frames-v2-starter/app/App.tsx index c5b5e65cf..25bb19140 100644 --- a/templates/next-frames-v2-starter/app/App.tsx +++ b/templates/next-frames-v2-starter/app/App.tsx @@ -1,3 +1,5 @@ +"use client"; + import { useEffect, useCallback, useState } from "react"; import sdk, { type FrameContext } from "@farcaster/frame-sdk"; import { @@ -14,7 +16,7 @@ import { config } from "./WagmiProvider"; import { Button } from "./ui/Button"; import { truncateAddress } from "./lib/truncateAddress"; -export default function Demo() { +export function App() { const [isSDKLoaded, setIsSDKLoaded] = useState(false); const [context, setContext] = useState(); const [isContextOpen, setIsContextOpen] = useState(false); @@ -65,6 +67,15 @@ export default function Demo() { sdk.actions.openUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ"); }, []); + const setPrimaryButton = useCallback(() => { + sdk.actions.setPrimaryButton({ + text: "Primary Button", + loading: false, + disabled: false, + hidden: false, + }); + }, []); + const close = useCallback(() => { sdk.actions.close(); }, []); @@ -72,8 +83,7 @@ export default function Demo() { const sendTx = useCallback(() => { sendTransaction( { - to: "0x4bBFD120d9f352A0BEd7a014bd67913a2007a878", - data: "0x9846cd9efc000023c0", + to: address, }, { onSuccess: (hash) => { @@ -81,7 +91,7 @@ export default function Demo() { }, } ); - }, [sendTransaction]); + }, [address, sendTransaction]); const sign = useCallback(() => { signMessage({ message: "Hello from Frames v2!" }); @@ -108,6 +118,16 @@ export default function Demo() { setIsContextOpen((prev) => !prev); }, []); + useEffect(() => { + const listener = () => console.log("Primary button clicked!"); + + sdk.on("primaryButtonClicked", listener); + + return () => { + sdk.off("primaryButtonClicked", listener); + }; + }, []); + const renderError = (error: Error | null) => { if (!error) return null; return
{error.message}
; @@ -149,6 +169,15 @@ export default function Demo() {

Actions

+
+
+
+              sdk.actions.setPrimaryButton
+            
+
+ +
+
diff --git a/templates/next-frames-v2-starter/app/WagmiProvider.tsx b/templates/next-frames-v2-starter/app/WagmiProvider.tsx
index 8f08c8887..360fa87fc 100644
--- a/templates/next-frames-v2-starter/app/WagmiProvider.tsx
+++ b/templates/next-frames-v2-starter/app/WagmiProvider.tsx
@@ -1,3 +1,4 @@
+"use client";
 import { createConfig, http, WagmiProvider } from "wagmi";
 import { base } from "wagmi/chains";
 import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
diff --git a/templates/next-frames-v2-starter/app/globals.css b/templates/next-frames-v2-starter/app/globals.css
index 6a7572500..8abdb15c9 100644
--- a/templates/next-frames-v2-starter/app/globals.css
+++ b/templates/next-frames-v2-starter/app/globals.css
@@ -1,7 +1,7 @@
 @tailwind base;
 @tailwind components;
 @tailwind utilities;
- 
+
 @layer base {
   :root {
     --background: 0 0% 100%;
@@ -9,63 +9,63 @@
 
     --card: 0 0% 100%;
     --card-foreground: 222.2 84% 4.9%;
- 
+
     --popover: 0 0% 100%;
     --popover-foreground: 222.2 84% 4.9%;
- 
+
     --primary: 222.2 47.4% 11.2%;
     --primary-foreground: 210 40% 98%;
- 
+
     --secondary: 210 40% 96.1%;
     --secondary-foreground: 222.2 47.4% 11.2%;
- 
+
     --muted: 210 40% 96.1%;
     --muted-foreground: 215.4 16.3% 46.9%;
- 
+
     --accent: 210 40% 96.1%;
     --accent-foreground: 222.2 47.4% 11.2%;
- 
+
     --destructive: 0 84.2% 60.2%;
     --destructive-foreground: 210 40% 98%;
 
     --border: 214.3 31.8% 91.4%;
     --input: 214.3 31.8% 91.4%;
     --ring: 222.2 84% 4.9%;
- 
+
     --radius: 0.5rem;
   }
- 
+
   .dark {
     --background: 222.2 84% 4.9%;
     --foreground: 210 40% 98%;
- 
+
     --card: 222.2 84% 4.9%;
     --card-foreground: 210 40% 98%;
- 
+
     --popover: 222.2 84% 4.9%;
     --popover-foreground: 210 40% 98%;
- 
+
     --primary: 210 40% 98%;
     --primary-foreground: 222.2 47.4% 11.2%;
- 
+
     --secondary: 217.2 32.6% 17.5%;
     --secondary-foreground: 210 40% 98%;
- 
+
     --muted: 217.2 32.6% 17.5%;
     --muted-foreground: 215 20.2% 65.1%;
- 
+
     --accent: 217.2 32.6% 17.5%;
     --accent-foreground: 210 40% 98%;
- 
+
     --destructive: 0 62.8% 30.6%;
     --destructive-foreground: 210 40% 98%;
- 
+
     --border: 217.2 32.6% 17.5%;
     --input: 217.2 32.6% 17.5%;
     --ring: 212.7 26.8% 83.9%;
   }
 }
- 
+
 @layer base {
   * {
     @apply border-border;
@@ -73,4 +73,4 @@
   body {
     @apply bg-background text-foreground;
   }
-}
\ No newline at end of file
+}
diff --git a/templates/next-frames-v2-starter/app/layout.tsx b/templates/next-frames-v2-starter/app/layout.tsx
index bd0e2fcd7..d49de8ebf 100644
--- a/templates/next-frames-v2-starter/app/layout.tsx
+++ b/templates/next-frames-v2-starter/app/layout.tsx
@@ -1,21 +1,6 @@
 import type { Metadata } from "next";
 import "./globals.css";
-
-// @todo refactor to frames.js or @frames.js/render?
-type FrameEmbed = {
-  imageUrl: string;
-  button: {
-    title: string;
-    action: {
-      type: "launch";
-      icon: string;
-      name: string;
-      url: string;
-      splashImageUrl: string;
-      splashBackgroundColor: string;
-    };
-  };
-};
+import type { FrameV2 } from "frames.js";
 
 export const metadata: Metadata = {
   // without a title, warpcast won't validate your frame
@@ -23,13 +8,13 @@ export const metadata: Metadata = {
   description: "...",
   other: {
     "fc:frame": JSON.stringify({
-      imageUrl: "https://example.com/image.png",
+      version: "next",
+      imageUrl: new URL("/frame.png", process.env.APP_URL!).toString(),
       button: {
         title: "Open App",
         action: {
-          type: "launch",
-          icon: new URL("/icon.png", process.env.APP_URL).toString(),
-          name: "Fremes v2 Demo",
+          type: "launch_frame",
+          name: "Frames v2 Demo",
           url: process.env.APP_URL!,
           splashImageUrl: new URL(
             "/splash.png",
@@ -38,7 +23,7 @@ export const metadata: Metadata = {
           splashBackgroundColor: "#f7f7f7",
         },
       },
-    } satisfies FrameEmbed),
+    } satisfies FrameV2),
   },
 };
 
diff --git a/templates/next-frames-v2-starter/app/page.tsx b/templates/next-frames-v2-starter/app/page.tsx
index 4b2758654..4564e0bcc 100644
--- a/templates/next-frames-v2-starter/app/page.tsx
+++ b/templates/next-frames-v2-starter/app/page.tsx
@@ -1,5 +1,5 @@
-import { WagmiConfig } from "./WagmiConfig";
-import { App } from "./app";
+import { WagmiConfig } from "./WagmiProvider";
+import { App } from "./App";
 
 export default async function Home() {
   return (
diff --git a/templates/next-frames-v2-starter/app/ui/Button.tsx b/templates/next-frames-v2-starter/app/ui/Button.tsx
index e5a8fc5b0..685954482 100644
--- a/templates/next-frames-v2-starter/app/ui/Button.tsx
+++ b/templates/next-frames-v2-starter/app/ui/Button.tsx
@@ -1,17 +1,16 @@
-interface ButtonProps extends React.ButtonHTMLAttributes {
-  children: React.ReactNode;
+import { Button as UIButton, ButtonProps as UIButtonProps } from '@/components/ui/button';
+
+type ButtonProps = UIButtonProps & {
   isLoading?: boolean;
 }
 
 export function Button({
   children,
-  className = "",
   isLoading = false,
   ...props
 }: ButtonProps) {
   return (
-    
+    
   );
 }
diff --git a/templates/next-frames-v2-starter/components.json b/templates/next-frames-v2-starter/components.json
new file mode 100644
index 000000000..15f2b0250
--- /dev/null
+++ b/templates/next-frames-v2-starter/components.json
@@ -0,0 +1,17 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "default",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "tailwind.config.ts",
+    "css": "app/globals.css",
+    "baseColor": "slate",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils"
+  }
+}
\ No newline at end of file
diff --git a/templates/next-frames-v2-starter/package.json b/templates/next-frames-v2-starter/package.json
index c6115d494..94c090076 100644
--- a/templates/next-frames-v2-starter/package.json
+++ b/templates/next-frames-v2-starter/package.json
@@ -11,7 +11,8 @@
     "lint": "next lint"
   },
   "dependencies": {
-    "@farcaster/frame-sdk": "^0.0.2",
+    "@farcaster/frame-sdk": "^0.0.3",
+    "@radix-ui/react-slot": "^1.1.0",
     "@tanstack/react-query": "^5.61.3",
     "clsx": "^2.1.0",
     "frames.js": "^0.20.0",
@@ -19,6 +20,7 @@
     "react": "^18.2.0",
     "react-dom": "^18.2.0",
     "tailwindcss-animate": "^1.0.7",
+    "tailwind-merge": "^2.2.1",
     "viem": "^2.21.50",
     "wagmi": "^2.13.0"
   },
diff --git a/templates/next-frames-v2-starter/public/frame.png b/templates/next-frames-v2-starter/public/frame.png
new file mode 100644
index 0000000000000000000000000000000000000000..b55ed7a188b4f05e657c86a8c4c033c3174aaca3
GIT binary patch
literal 536945
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

OTN3Aypwo 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�&~LWluOB-s&`7&;O;i!R?su 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 +BcS(Nj8c#l5T>C3;MMdFJFH*bMlH< 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�^wlKu7Myy4y_#wLw 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

-pjKSyf4;)}zjGi?9MK;){!@;y2KLNQ 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

QBEriUi9uY9f%0(e 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
E**a*dtG2NHMD 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 $oW5u7y0N$Ufxh44>IRQ4p)H4@ zBrmg#|Cy5~bwVfPoEpxT-$(rX^x|rN?H$+nF#h8meQsC7`mO%sZ&|%=tT@)xERTz@ zH~-(`<1h1LUzBP)*8JB3&g@+LpNniSw0$Bf1Qi+c)z`A&>^Y_9nAc8fu*V`h-@ z)%$4oW4sZ|bvTUW;z*yDb zYR>%c_&5@cvzft(A;mX?Fn-jKAfyPq4?j11o}daYRxx2edymSK3`&H znJad5WWMs>k?*%YY$Vk^aWzL+c7Y^p8sl~2>!(f&UTwta|2fp3F(OwE7rXy8i(Cug z_*em>yHyy6Ulx?SY;m3c3;z@PI=t^Zy)YVZSN->ru!YE}2wON{yPW@w|I25vA(5

tGn)a(hQO)Pi6nrsOIR5 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ƒ*e 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 Date: Fri, 29 Nov 2024 14:56:01 +0100 Subject: [PATCH 21/88] fix: test --- packages/frames.js/src/getFrame.test.ts | 4 ++++ packages/frames.js/src/getFrameHtml.test.ts | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/packages/frames.js/src/getFrame.test.ts b/packages/frames.js/src/getFrame.test.ts index 411823aab..52ce421ac 100644 --- a/packages/frames.js/src/getFrame.test.ts +++ b/packages/frames.js/src/getFrame.test.ts @@ -244,6 +244,10 @@ describe("getFrame", () => { const result = getFrame({ htmlString: html, url: "https://example.com" }); + if (result.specification !== "farcaster") { + throw new Error("Specification should be farcaster"); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is test expect(JSON.parse(result.frame.state!)).toEqual({ test: "'><&", diff --git a/packages/frames.js/src/getFrameHtml.test.ts b/packages/frames.js/src/getFrameHtml.test.ts index f814d829f..b12f3a2fd 100644 --- a/packages/frames.js/src/getFrameHtml.test.ts +++ b/packages/frames.js/src/getFrameHtml.test.ts @@ -19,6 +19,12 @@ describe("getFrameHtmlHead", () => { const result = getFrame({ htmlString: html, url: "http://framesjs.org" }); + if (result.specification !== "farcaster") { + throw new Error( + `Expected result to be a Farcaster frame but got ${result.specification}` + ); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is test expect(JSON.parse(result.frame.state!)).toEqual(json); }); @@ -39,6 +45,12 @@ describe("getFrameHtmlHead", () => { const result = getFrame({ htmlString: html, url: "http://framesjs.org" }); + if (result.specification !== "farcaster") { + throw new Error( + `Expected result to be a Farcaster frame but got ${result.specification}` + ); + } + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- this is test expect(JSON.parse(result.frame.state!)).toEqual(json); }); From 3102c93d2fcb2df24800c39ebf52436db5c020fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 29 Nov 2024 15:20:24 +0100 Subject: [PATCH 22/88] test: add farcaster v2 tests --- .../src/frame-parsers/farcasterV2.test.ts | 990 ++++++++++++++++++ .../src/frame-parsers/farcasterV2.ts | 75 +- 2 files changed, 1020 insertions(+), 45 deletions(-) create mode 100644 packages/frames.js/src/frame-parsers/farcasterV2.test.ts diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.test.ts b/packages/frames.js/src/frame-parsers/farcasterV2.test.ts new file mode 100644 index 000000000..df5101b08 --- /dev/null +++ b/packages/frames.js/src/frame-parsers/farcasterV2.test.ts @@ -0,0 +1,990 @@ +/* eslint-disable @typescript-eslint/no-explicit-any -- tests */ +/* eslint-disable @typescript-eslint/no-unsafe-assignment -- tests */ +import { load } from "cheerio"; +import type { PartialDeep } from "type-fest"; +import type { FrameV2 } from "../types"; +import { parseFarcasterFrameV2 } from "./farcasterV2"; +import { createReporter } from "./reporter"; + +const validFrame: FrameV2 = { + button: { + action: { + name: "App name", + splashBackgroundColor: "#000000", + splashImageUrl: "https://framesjs.org/logo.png", + url: "https://framesjs.org", + type: "launch_frame", + }, + title: "Button title", + }, + imageUrl: "https://framesjs.org/logo.png", + version: "next", +}; + +describe("farcaster frame v2 parser", () => { + let reporter = createReporter("farcaster_v2"); + + beforeEach(() => { + reporter = createReporter("farcaster_v2"); + }); + + it("does not support farcaster v1 metatags", () => { + const document = load(` + + + + Test + `); + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + reports: { + "fc:frame": [ + { + level: "error", + source: "farcaster_v2", + message: "Failed to parse Frame, it is not a valid JSON value", + }, + ], + }, + frame: {}, + }); + }); + + it('parses frame from "fc:frame" meta tag', () => { + const document = load(` + + Test + `); + + expect(parseFarcasterFrameV2(document, { reporter })).toEqual({ + status: "success", + specification: "farcaster_v2", + frame: { + version: "next", + imageUrl: "https://framesjs.org/logo.png", + button: { + action: { + name: "App name", + splashBackgroundColor: "#000000", + splashImageUrl: "https://framesjs.org/logo.png", + url: "https://framesjs.org", + type: "launch_frame", + }, + title: "Button title", + }, + }, + reports: {}, + }); + }); + + it("fails on missing version", () => { + const document = load(` + + Test + `); + + const { version: _, ...restOfFrame } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "version" in Frame', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string version", (version) => { + const document = load(` + + Test + `); + + const { version: _, ...restOfFrame } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "version" in Frame must be a string', + }, + ], + }, + }); + }); + + it("fails on missing imageUrl", () => { + const document = load(` + + Test + `); + + const { imageUrl: _, ...restOfFrame } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "imageUrl" in Frame', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string imageUrl", (imageUrl) => { + const document = load(` + + Test + `); + + const { imageUrl: _, ...restOfFrame } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "imageUrl" in Frame must be a string', + }, + ], + }, + }); + }); + + it("fails on invalid URL in imageUrl", () => { + const document = load(` + + Test + `); + + const { imageUrl: _, ...restOfFrame } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "imageUrl" in Frame must be a valid URL', + }, + ], + }, + }); + }); + + describe("button", () => { + it("fails on missing title", () => { + const document = load(` + + Test + `); + + const { + button: { title: _, ...restOfButton }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "title" in Frame.button', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string title", (title) => { + const document = load(` + + Test + `); + + const { + button: { title: _, ...restOfButton }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "title" in Frame.button must be a string', + }, + ], + }, + }); + }); + + describe("action", () => { + it("fails on missing name", () => { + const document = load(` + + Test + `); + + const { + button: { + action: { name: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "name" in Frame.button.action', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string name", (name) => { + const document = load(` + + Test + `); + + const { + button: { + action: { name: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "name" in Frame.button.action must be a string', + }, + ], + }, + }); + }); + + it("fails on missing type", () => { + const document = load(` + + Test + `); + + const { + button: { + action: { type: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "type" in Frame.button.action', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string type", (type) => { + const document = load(` + + Test + `); + + const { + button: { + action: { type: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "type" in Frame.button.action must be a string', + }, + ], + }, + }); + }); + + it('fails on invalid type, must be "launch_frame"', () => { + const document = load(` + + Test + `); + + const { + button: { + action: { type: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Key "type" in Frame.button.action must be "launch_frame"', + }, + ], + }, + }); + }); + + it("fails on missing url", () => { + const document = load(` + + Test + `); + + const { + button: { + action: { url: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Missing required key "url" in Frame.button.action', + }, + ], + }, + }); + }); + + it.each([1, true, null])("fails to parse non string url", (url) => { + const document = load(` + + Test + `); + + const { + button: { + action: { url: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "url" in Frame.button.action must be a string', + }, + ], + }, + }); + }); + + it("fails if url is not valid URL", () => { + const document = load(` + + Test + `); + + const { + button: { + action: { url: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: 'Key "url" in Frame.button.action must be a valid URL', + }, + ], + }, + }); + }); + + it('fails on missing "splashImageUrl"', () => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashImageUrl: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Missing required key "splashImageUrl" in Frame.button.action', + }, + ], + }, + }); + }); + + it.each([1, true, null])( + 'fails to parse non string "splashImageUrl"', + (splashImageUrl) => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashImageUrl: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Key "splashImageUrl" in Frame.button.action must be a string', + }, + ], + }, + }); + } + ); + + it('fails on invalid "splashImageUrl" URL', () => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashImageUrl: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Key "splashImageUrl" in Frame.button.action must be a valid URL', + }, + ], + }, + }); + }); + + it('fails on missing "splashBackgroundColor"', () => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashBackgroundColor: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Missing required key "splashBackgroundColor" in Frame.button.action', + }, + ], + }, + }); + }); + + it.each([1, true, null])( + 'fails to parse non string "splashBackgroundColor"', + (splashBackgroundColor) => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashBackgroundColor: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Key "splashBackgroundColor" in Frame.button.action must be a string', + }, + ], + }, + }); + } + ); + + it('fails on invalid "splashBackgroundColor" color', () => { + const document = load(` + + Test + `); + + const { + button: { + action: { splashBackgroundColor: _, ...restOfAction }, + ...restOfButton + }, + ...restOfFrame + } = validFrame; + + expect(parseFarcasterFrameV2(document, { reporter })).toMatchObject({ + status: "failure", + specification: "farcaster_v2", + frame: { + ...restOfFrame, + button: { + ...restOfButton, + action: { + ...restOfAction, + }, + }, + }, + reports: { + "fc:frame": [ + { + source: "farcaster_v2", + level: "error", + message: + 'Key "splashBackgroundColor" in Frame.button.action must be a valid hex color', + }, + ], + }, + }); + }); + }); + }); +}); diff --git a/packages/frames.js/src/frame-parsers/farcasterV2.ts b/packages/frames.js/src/frame-parsers/farcasterV2.ts index fc8c52778..86b61bdb2 100644 --- a/packages/frames.js/src/frame-parsers/farcasterV2.ts +++ b/packages/frames.js/src/frame-parsers/farcasterV2.ts @@ -34,7 +34,7 @@ export function parseFarcasterFrameV2( } catch (error) { reporter.error( "fc:frame", - "Failed to parse FrameEmbed it is not a valid JSON value" + "Failed to parse Frame, it is not a valid JSON value" ); return { @@ -46,7 +46,7 @@ export function parseFarcasterFrameV2( } if (typeof parsedJSON !== "object") { - reporter.error("fc:frame", "FrameEmbed must be an object"); + reporter.error("fc:frame", "Frame must be an object"); return { status: "failure", @@ -57,7 +57,7 @@ export function parseFarcasterFrameV2( } if (parsedJSON === null) { - reporter.error("fc:frame", "FrameEmbed must not be null"); + reporter.error("fc:frame", "Frame must not be null"); return { status: "failure", @@ -68,22 +68,19 @@ export function parseFarcasterFrameV2( } if (!("version" in parsedJSON)) { - reporter.error("fc:frame", 'Missing required key "version" in FrameEmbed'); + reporter.error("fc:frame", 'Missing required key "version" in Frame'); } else if (typeof parsedJSON.version !== "string") { - reporter.error("fc:frame", 'Key "version" in FrameEmbed must be a string'); + reporter.error("fc:frame", 'Key "version" in Frame must be a string'); } else { parsedFrame.version = parsedJSON.version; } if (!("imageUrl" in parsedJSON)) { - reporter.error("fc:frame", 'Missing required key "imageUrl" in FrameEmbed'); + reporter.error("fc:frame", 'Missing required key "imageUrl" in Frame'); } else if (typeof parsedJSON.imageUrl !== "string") { - reporter.error("fc:frame", 'Key "imageUrl" in FrameEmbed must be a string'); + reporter.error("fc:frame", 'Key "imageUrl" in Frame must be a string'); } else if (!URL.canParse(parsedJSON.imageUrl)) { - reporter.error( - "fc:frame", - 'Key "imageUrl" in FrameEmbed must be a valid URL' - ); + reporter.error("fc:frame", 'Key "imageUrl" in Frame must be a valid URL'); } else { parsedFrame.imageUrl = parsedJSON.imageUrl; } @@ -91,7 +88,7 @@ export function parseFarcasterFrameV2( // @todo add optional validation for frame image size if (!("button" in parsedJSON)) { - reporter.error("fc:frame", 'Missing required key "button" in FrameEmbed'); + reporter.error("fc:frame", 'Missing required key "button" in Frame'); } else { parsedFrame.button = parseFrameButton(parsedJSON.button, reporter); } @@ -118,13 +115,13 @@ function parseFrameButton( reporter: Reporter ): ParsedFrameV2["button"] { if (typeof parsedValue !== "object") { - reporter.error("fc:frame", 'Key "button" in FrameEmbed must be an object'); + reporter.error("fc:frame", 'Key "button" in Frame must be an object'); return {}; } if (parsedValue === null) { - reporter.error("fc:frame", 'Key "button" in FrameEmbed must not be null'); + reporter.error("fc:frame", 'Key "button" in Frame must not be null'); return {}; } @@ -132,24 +129,15 @@ function parseFrameButton( const button: ParsedFrameV2["button"] = {}; if (!("title" in parsedValue)) { - reporter.error( - "fc:frame", - 'Missing required key "title" in FrameEmbed.button' - ); + reporter.error("fc:frame", 'Missing required key "title" in Frame.button'); } else if (typeof parsedValue.title !== "string") { - reporter.error( - "fc:frame", - 'Key "title" in FrameEmbed.button must be a string' - ); + reporter.error("fc:frame", 'Key "title" in Frame.button must be a string'); } else { button.title = parsedValue.title; } if (!("action" in parsedValue)) { - reporter.error( - "fc:frame", - 'Missing required key "action" in FrameEmbed.button' - ); + reporter.error("fc:frame", 'Missing required key "action" in Frame.button'); } else { button.action = parseFrameButtonAction(parsedValue.action, reporter); } @@ -164,17 +152,14 @@ function parseFrameButtonAction( if (typeof parsedValue !== "object") { reporter.error( "fc:frame", - 'Key "action" in FrameEmbed.button must be an object' + 'Key "action" in Frame.button must be an object' ); return {}; } if (parsedValue === null) { - reporter.error( - "fc:frame", - 'Key "action" in FrameEmbed.button must not be null' - ); + reporter.error("fc:frame", 'Key "action" in Frame.button must not be null'); return {}; } @@ -184,12 +169,12 @@ function parseFrameButtonAction( if (!("name" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "name" in FrameEmbed.button.action' + 'Missing required key "name" in Frame.button.action' ); } else if (typeof parsedValue.name !== "string") { reporter.error( "fc:frame", - 'Key "name" in FrameEmbed.button.action must be a string' + 'Key "name" in Frame.button.action must be a string' ); } else { action.name = parsedValue.name; @@ -198,17 +183,17 @@ function parseFrameButtonAction( if (!("type" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "type" in FrameEmbed.button.action' + 'Missing required key "type" in Frame.button.action' ); } else if (typeof parsedValue.type !== "string") { reporter.error( "fc:frame", - 'Key "type" in FrameEmbed.button.action must be a string' + 'Key "type" in Frame.button.action must be a string' ); } else if (parsedValue.type !== "launch_frame") { reporter.error( "fc:frame", - 'Key "type" in FrameEmbed.button.action must be "launch_frame"' + 'Key "type" in Frame.button.action must be "launch_frame"' ); } else { action.type = parsedValue.type; @@ -217,17 +202,17 @@ function parseFrameButtonAction( if (!("url" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "url" in FrameEmbed.button.action' + 'Missing required key "url" in Frame.button.action' ); } else if (typeof parsedValue.url !== "string") { reporter.error( "fc:frame", - 'Key "url" in FrameEmbed.button.action must be a string' + 'Key "url" in Frame.button.action must be a string' ); } else if (!URL.canParse(parsedValue.url)) { reporter.error( "fc:frame", - 'Key "url" in FrameEmbed.button.action must be a valid URL' + 'Key "url" in Frame.button.action must be a valid URL' ); } else { action.url = parsedValue.url; @@ -237,17 +222,17 @@ function parseFrameButtonAction( if (!("splashImageUrl" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "splashImageUrl" in FrameEmbed.button.action' + 'Missing required key "splashImageUrl" in Frame.button.action' ); } else if (typeof parsedValue.splashImageUrl !== "string") { reporter.error( "fc:frame", - 'Key "splashImageUrl" in FrameEmbed.button.action must be a string' + 'Key "splashImageUrl" in Frame.button.action must be a string' ); } else if (!URL.canParse(parsedValue.splashImageUrl)) { reporter.error( "fc:frame", - 'Key "splashImageUrl" in FrameEmbed.button.action must be a valid URL' + 'Key "splashImageUrl" in Frame.button.action must be a valid URL' ); } else { action.splashImageUrl = parsedValue.splashImageUrl; @@ -256,17 +241,17 @@ function parseFrameButtonAction( if (!("splashBackgroundColor" in parsedValue)) { reporter.error( "fc:frame", - 'Missing required key "splashBackgroundColor" in FrameEmbed.button.action' + 'Missing required key "splashBackgroundColor" in Frame.button.action' ); } else if (typeof parsedValue.splashBackgroundColor !== "string") { reporter.error( "fc:frame", - 'Key "splashBackgroundColor" in FrameEmbed.button.action must be a string' + 'Key "splashBackgroundColor" in Frame.button.action must be a string' ); } else if (!/^#[0-9a-fA-F]{6,8}$/.test(parsedValue.splashBackgroundColor)) { reporter.error( "fc:frame", - 'Key "splashBackgroundColor" in FrameEmbed.button.action must be a valid hex color' + 'Key "splashBackgroundColor" in Frame.button.action must be a valid hex color' ); } else { action.splashBackgroundColor = parsedValue.splashBackgroundColor; From 060b8b4105875ee489c71d9ed592a7b8e77d2ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Mon, 2 Dec 2024 13:55:35 +0100 Subject: [PATCH 23/88] chore: update sdk --- packages/render/package.json | 4 +- templates/next-frames-v2-starter/package.json | 2 +- yarn.lock | 61 +++++++++---------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/packages/render/package.json b/packages/render/package.json index 05e5aea23..bc337cafe 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -303,6 +303,7 @@ "@types/react": "^18.2.0", "@xmtp/xmtp-js": "^12.0.0", "@xmtp/frames-client": "^0.5.3", + "react-native-webview": "^13.12.4", "tsup": "^8.0.1", "typescript": "^5.4.5", "viem": "^2.13.7", @@ -319,11 +320,12 @@ "next": "^14.1.0", "react": "^18.2.0", "react-native": "^0.74.3", + "react-native-webview": "^13.12.4", "viem": "^2.7.8", "wagmi": "^2.9.10" }, "dependencies": { - "@farcaster/frame-sdk": "^0.0.3", + "@farcaster/frame-sdk": "^0.0.5", "@farcaster/core": "^0.14.7", "@noble/ed25519": "^2.0.0", "comlink": "^4.4.2", diff --git a/templates/next-frames-v2-starter/package.json b/templates/next-frames-v2-starter/package.json index 94c090076..fec696e78 100644 --- a/templates/next-frames-v2-starter/package.json +++ b/templates/next-frames-v2-starter/package.json @@ -11,7 +11,7 @@ "lint": "next lint" }, "dependencies": { - "@farcaster/frame-sdk": "^0.0.3", + "@farcaster/frame-sdk": "^0.0.5", "@radix-ui/react-slot": "^1.1.0", "@tanstack/react-query": "^5.61.3", "clsx": "^2.1.0", diff --git a/yarn.lock b/yarn.lock index 636b4b9ea..9cea2a9ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2419,14 +2419,15 @@ dependencies: ox "^0.1.6" -"@farcaster/frame-sdk@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.3.tgz#72159b447736ea0842c3bd08d0506f36eb787cd8" - integrity sha512-p9S7SSbIhOL/o9VIkbdlGqG7ooTopECm1PzxsZx/5CF+rC7xaqQQcwG4YgqIOg030tq1GprXFSChKCe7VJP3OA== +"@farcaster/frame-sdk@^0.0.5": + version "0.0.5" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.5.tgz#f4ec1381233dfc9b15adc01ef8c0adf42ea4d1b4" + integrity sha512-oSsfnkvHVMHMEcXru7XMX7u6H3SaHTqiGdxUq/lChLR7PqwqgNAHfmjA+HW6TV2GUjOqmr94/82atO3+pa9UUw== dependencies: "@farcaster/frame-core" "^0.0.4" comlink "^4.4.2" eventemitter3 "^5.0.1" + ox "^0.2.2" "@fastify/busboy@^2.0.0": version "2.1.1" @@ -15117,6 +15118,19 @@ ox@^0.1.6: abitype "^1.0.6" eventemitter3 "5.0.1" +ox@^0.2.2: + version "0.2.2" + resolved "https://registry.yarnpkg.com/ox/-/ox-0.2.2.tgz#b177912d2fd9853e52c2db8570ac29c09330cec1" + integrity sha512-QWCyFfVk5hFOhg13SGqRKih5B7EBucrf+Z1dfmN9jJQ8MZdrRx9mbD78JQL5ogSzDT7fcHgyMCaXd/3AWn6xHQ== + dependencies: + "@adraffy/ens-normalize" "^1.10.1" + "@noble/curves" "^1.6.0" + "@noble/hashes" "^1.5.0" + "@scure/bip32" "^1.5.0" + "@scure/bip39" "^1.4.0" + abitype "^1.0.6" + eventemitter3 "5.0.1" + p-filter@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/p-filter/-/p-filter-2.1.0.tgz#1b1472562ae7a0f742f0f3d3d3718ea66ff9c09c" @@ -16010,6 +16024,14 @@ react-native-webview@^11.26.0: escape-string-regexp "2.0.0" invariant "2.2.4" +react-native-webview@^13.12.4: + version "13.12.4" + resolved "https://registry.yarnpkg.com/react-native-webview/-/react-native-webview-13.12.4.tgz#1a563a7fbd6bf53d688388d46708f273ed0ebb94" + integrity sha512-8lWeYPVWeOj0ya9ZpDesOQPRgczuN3ogQHlhS21sNXndd4kvfPG+WjlRdrvxYgj//udpwmzcWzagwLnEp60Aqg== + dependencies: + escape-string-regexp "^4.0.0" + invariant "2.2.4" + react-native@*: version "0.74.3" resolved "https://registry.yarnpkg.com/react-native/-/react-native-0.74.3.tgz#eef32cd10afb1f4b26f75b79eefd6b220c63953c" @@ -17276,16 +17298,7 @@ string-length@^4.0.1: char-regex "^1.0.2" strip-ansi "^6.0.0" -"string-width-cjs@npm:string-width@^4.2.0": - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - -string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -17385,7 +17398,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -17399,13 +17412,6 @@ strip-ansi@^5.0.0, strip-ansi@^5.2.0: dependencies: ansi-regex "^4.1.0" -strip-ansi@^6.0.0, strip-ansi@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - strip-ansi@^7.0.1, strip-ansi@^7.1.0: version "7.1.0" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" @@ -19047,7 +19053,7 @@ wrangler@3.39.0, wrangler@^3.39.0: optionalDependencies: fsevents "~2.3.2" -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -19065,15 +19071,6 @@ wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 344fc74653d20b272af344d22dbcec11ffa023e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Mon, 2 Dec 2024 15:00:54 +0100 Subject: [PATCH 24/88] fix: unregister exposed comlink listeners --- .../app/components/frame-app-dialog.tsx | 16 ++-- packages/render/package.json | 2 +- packages/render/src/unstable-use-frame-app.ts | 86 ++++++++++++++++--- yarn.lock | 5 ++ 4 files changed, 88 insertions(+), 21 deletions(-) diff --git a/packages/debugger/app/components/frame-app-dialog.tsx b/packages/debugger/app/components/frame-app-dialog.tsx index 80b8087a3..1ee755ef9 100644 --- a/packages/debugger/app/components/frame-app-dialog.tsx +++ b/packages/debugger/app/components/frame-app-dialog.tsx @@ -17,6 +17,7 @@ import type { FarcasterMultiSignerInstance } from "@frames.js/render/identity/fa import { Loader2Icon } from "lucide-react"; import { useWalletClient } from "wagmi"; import { Button } from "@/components/ui/button"; +import { useToast } from "@/components/ui/use-toast"; type FrameAppDialogProps = { farcasterSigner: FarcasterMultiSignerInstance; @@ -32,6 +33,7 @@ export function FrameAppDialog({ frameState, onClose, }: FrameAppDialogProps) { + const { toast } = useToast(); const walletClient = useWalletClient(); const [isReady, setIsReady] = useState(false); const [primaryButton, setPrimaryButton] = useState( @@ -50,7 +52,6 @@ export function FrameAppDialog({ }, onPrimaryButtonSet: setPrimaryButton, }); - const iframeRef = useRef(null); const { name, url, splashImageUrl, splashBackgroundColor } = frameState.frame.button.action; @@ -93,7 +94,6 @@ export function FrameAppDialog({ )} + {!isLoadingWallet && ( + + )}

{primaryButton && !primaryButton.hidden && ( From e3e81afe8bec0fe2ab46c8b2740365f3443c0510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 14:26:03 +0100 Subject: [PATCH 48/88] feat: allow to use different connectors --- packages/render/package.json | 35 +++- .../src/frame-app/provider/event-emitter.ts | 97 ++++++++++ .../render/src/frame-app/provider/types.ts | 32 ++++ .../render/src/frame-app/provider/wagmi.ts | 94 +++++++++ packages/render/src/frame-app/types.ts | 23 +++ ...able-use-frame-app.ts => use-frame-app.ts} | 180 ++++++++++-------- yarn.lock | 18 +- 7 files changed, 381 insertions(+), 98 deletions(-) create mode 100644 packages/render/src/frame-app/provider/event-emitter.ts create mode 100644 packages/render/src/frame-app/provider/types.ts create mode 100644 packages/render/src/frame-app/provider/wagmi.ts create mode 100644 packages/render/src/frame-app/types.ts rename packages/render/src/{unstable-use-frame-app.ts => use-frame-app.ts} (88%) diff --git a/packages/render/package.json b/packages/render/package.json index 6be52e0aa..5af53f32c 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -86,6 +86,26 @@ "default": "./dist/farcaster/index.cjs" } }, + "./frame-app/provider/event-emitter": { + "import": { + "types": "./dist/frame-app/provider/event-emitter.d.ts", + "default": "./dist/frame-app/provider/event-emitter.js" + }, + "require": { + "types": "./dist/frame-app/provider/event-emitter.d.cts", + "default": "./dist/frame-app/provider/event-emitter.cjs" + } + }, + "./frame-app/provider/wagmi": { + "import": { + "types": "./dist/frame-app/provider/wagmi.d.ts", + "default": "./dist/frame-app/provider/wagmi.js" + }, + "require": { + "types": "./dist/frame-app/provider/wagmi.d.cts", + "default": "./dist/frame-app/provider/wagmi.cjs" + } + }, "./ui": { "react-native": { "types": "./dist/ui/index.native.d.cts", @@ -180,14 +200,14 @@ "default": "./dist/unstable-use-fetch-frame.cjs" } }, - "./unstable-use-frame-app": { + "./use-frame-app": { "import": { - "types": "./dist/unstable-use-frame-app.d.ts", - "default": "./dist/unstable-use-frame-app.js" + "types": "./dist/use-frame-app.d.ts", + "default": "./dist/use-frame-app.js" }, "require": { - "types": "./dist/unstable-use-frame-app.d.cts", - "default": "./dist/unstable-use-frame-app.cjs" + "types": "./dist/use-frame-app.d.cts", + "default": "./dist/use-frame-app.cjs" } }, "./unstable-use-frame-state": { @@ -296,7 +316,7 @@ "src" ], "devDependencies": { - "@farcaster/frame-sdk": "^0.0.10", + "@farcaster/frame-sdk": "^0.0.15", "@lens-protocol/client": "^2.3.2", "@rainbow-me/rainbowkit": "^2.1.2", "@remix-run/node": "^2.8.1", @@ -312,7 +332,7 @@ }, "license": "MIT", "peerDependencies": { - "@farcaster/frame-sdk": "^0.0.10", + "@farcaster/frame-sdk": "^0.0.11", "@lens-protocol/client": "^2.0.0", "@rainbow-me/rainbowkit": "^2.1.2", "@types/react": "^18.2.0", @@ -330,6 +350,7 @@ "@farcaster/core": "^0.14.7", "@michalkvasnicak/comlink": "^4.5.1", "@noble/ed25519": "^2.0.0", + "eventemitter3": "^5.0.1", "frames.js": "^0.20.0", "ox": "^0.4.0", "zod": "^3.23.8" diff --git a/packages/render/src/frame-app/provider/event-emitter.ts b/packages/render/src/frame-app/provider/event-emitter.ts new file mode 100644 index 000000000..e9fdef750 --- /dev/null +++ b/packages/render/src/frame-app/provider/event-emitter.ts @@ -0,0 +1,97 @@ +import type { Endpoint } from "@michalkvasnicak/comlink"; +import { EventEmitter } from "eventemitter3"; +import type { FrameEthProviderEvent } from "../types"; +import type { + EthProviderEventEmitterEventMap, + EthProviderEventEmitterEvents, + EthProviderEventEmitterInterface, +} from "./types"; + +type DisconnectEndpointFunction = () => void; + +export class EthProviderEventEmitter + extends EventEmitter + implements EthProviderEventEmitterInterface +{ + private debugMode = false; + + forwardToEndpoint(endpoint: Endpoint): DisconnectEndpointFunction { + const forwardEvent = (event: EthProviderEventEmitterEvents): void => { + switch (event.event) { + case "accountsChanged": { + endpoint.postMessage({ + ...event, + type: "frameEthProviderEvent", + } satisfies FrameEthProviderEvent); + + return; + } + case "chainChanged": { + endpoint.postMessage({ + ...event, + type: "frameEthProviderEvent", + } satisfies FrameEthProviderEvent); + + return; + } + case "connect": { + endpoint.postMessage({ + ...event, + type: "frameEthProviderEvent", + } satisfies FrameEthProviderEvent); + + return; + } + case "disconnect": { + endpoint.postMessage({ + ...event, + type: "frameEthProviderEvent", + } satisfies FrameEthProviderEvent); + + return; + } + case "message": { + endpoint.postMessage({ + ...event, + type: "frameEthProviderEvent", + } satisfies FrameEthProviderEvent); + + return; + } + default: { + if (!this.debugMode) { + return; + } + + // eslint-disable-next-line no-console -- provide feedback to developer + console.warn( + "[@frames.js/render/frame-app/provider/event-emitter] Event not supported", + event + ); + } + } + }; + + this.on("accountsChanged", forwardEvent); + this.on("chainChanged", forwardEvent); + this.on("connect", forwardEvent); + this.on("disconnect", forwardEvent); + this.on("message", forwardEvent); + + return () => { + this.off("accountsChanged", forwardEvent); + this.off("chainChanged", forwardEvent); + this.off("connect", forwardEvent); + this.off("disconnect", forwardEvent); + this.off("message", forwardEvent); + }; + } + + setDebugMode(enabled: boolean): void { + this.debugMode = enabled; + } +} + +export function createEmitter(): EthProviderEventEmitterInterface { + return new EthProviderEventEmitter(); +} diff --git a/packages/render/src/frame-app/provider/types.ts b/packages/render/src/frame-app/provider/types.ts new file mode 100644 index 000000000..1c321ca3e --- /dev/null +++ b/packages/render/src/frame-app/provider/types.ts @@ -0,0 +1,32 @@ +import type { Provider, RpcSchema } from "ox"; +import type { EthProviderWireEvent } from "@farcaster/frame-sdk"; +import type { EventEmitter } from "eventemitter3"; +import type { Endpoint } from "@michalkvasnicak/comlink"; + +export type EthProviderRequestFunction = Provider.RequestFn; + +export type EthProviderEventEmitterEvents = EthProviderWireEvent; + +export type EthProviderEventEmitterEventMap = { + [K in EthProviderWireEvent["event"]]: [ + Extract, + ]; +}; + +export interface EthProviderEventEmitterInterface + extends EventEmitter { + /** + * Connects the endpoint to the event emitter, this means + * that any known events will be forwarded to the endpoint. + * + * Returns a function that disconnects the endpoint from the event emitter. + */ + forwardToEndpoint: (endpoint: Endpoint) => () => void; + + setDebugMode: (enabled: boolean) => void; +} + +export type EthProvider = { + readonly emitter: EthProviderEventEmitterInterface; + readonly request: EthProviderRequestFunction; +}; diff --git a/packages/render/src/frame-app/provider/wagmi.ts b/packages/render/src/frame-app/provider/wagmi.ts new file mode 100644 index 000000000..b61208ac8 --- /dev/null +++ b/packages/render/src/frame-app/provider/wagmi.ts @@ -0,0 +1,94 @@ +import { useAccount, useConfig, type ConnectorEventMap } from "wagmi"; +import { getWalletClient } from "wagmi/actions"; +import { useEffect, useRef } from "react"; +import { useFreshRef } from "../../hooks/use-fresh-ref"; +import type { EthProvider, EthProviderEventEmitterInterface } from "./types"; +import { createEmitter } from "./event-emitter"; + +export function useWagmiProvider(): EthProvider { + const account = useAccount(); + const config = useConfig(); + const configRef = useFreshRef(config); + const providerRef = useRef(null); + const emitterRef = useRef(null); + + useEffect(() => { + const connector = account.connector; + + if (!connector) { + return; + } + + function redispatchConnectEvent(event: ConnectorEventMap["connect"]): void { + emitterRef.current?.emit("connect", { + event: "connect", + params: [ + { + chainId: event.chainId.toString(), + }, + ], + }); + } + function redispatchDisconnectEvent(): void { + emitterRef.current?.emit("disconnect", { + event: "disconnect", + // @ts-expect-error -- this is weird, why there must be an error? + params: [], + }); + } + function redispatchMessageEvent(event: ConnectorEventMap["message"]): void { + emitterRef.current?.emit("message", { + event: "message", + params: [ + // @ts-expect-error -- this is correct but upstream type is requiring data property which is optional in wagmi + event, + ], + }); + } + function redispatchChangeEvent(event: ConnectorEventMap["change"]): void { + if (event.accounts) { + emitterRef.current?.emit("accountsChanged", { + event: "accountsChanged", + params: [event.accounts], + }); + } else if (event.chainId) { + emitterRef.current?.emit("chainChanged", { + event: "chainChanged", + params: [event.chainId.toString()], + }); + } + } + + connector.emitter.on("connect", redispatchConnectEvent); + connector.emitter.on("disconnect", redispatchDisconnectEvent); + connector.emitter.on("message", redispatchMessageEvent); + connector.emitter.on("change", redispatchChangeEvent); + + return () => { + connector.emitter.off("connect", redispatchConnectEvent); + connector.emitter.off("disconnect", redispatchDisconnectEvent); + connector.emitter.off("message", redispatchMessageEvent); + connector.emitter.off("change", redispatchChangeEvent); + }; + }, [account.connector]); + + // we don't want to call createEmitter repeately + if (!emitterRef.current) { + emitterRef.current = createEmitter(); + } + + // we don't want to reinstaniate the provider repeately + if (!providerRef.current) { + providerRef.current = { + emitter: emitterRef.current, + async request(params) { + const walletClient = await getWalletClient(configRef.current); + + // @ts-expect-error(2345) -- this is correct but params are typed by ox library so different type but shape is the same + return walletClient.request(params); + }, + } satisfies EthProvider; + } + + return providerRef.current; +} diff --git a/packages/render/src/frame-app/types.ts b/packages/render/src/frame-app/types.ts new file mode 100644 index 000000000..946f4f644 --- /dev/null +++ b/packages/render/src/frame-app/types.ts @@ -0,0 +1,23 @@ +import type { EthProviderWireEvent, FrameContext } from "@farcaster/frame-sdk"; + +export type FrameEthProviderEvent = { + type: "frameEthProviderEvent"; +} & EthProviderWireEvent; + +export type FrameEvent = { + type: "frameEvent"; + event: "primaryButtonClicked"; +}; + +/** + * This is here just because it is incosistent in @farcaster/frame-sdk + * Eventually this will be removed if they fix that. + * + * This is just a type that is used as a lead how to convert FrameEvent to FrameEventReactNative + * in react native event bridge implementation. + */ +export type FrameEventReactNative = { + type: "primaryButtonClicked"; +}; + +export type FrameClientConfig = FrameContext["client"]; diff --git a/packages/render/src/unstable-use-frame-app.ts b/packages/render/src/use-frame-app.ts similarity index 88% rename from packages/render/src/unstable-use-frame-app.ts rename to packages/render/src/use-frame-app.ts index da26bbdf3..f3a405d6e 100644 --- a/packages/render/src/unstable-use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -10,7 +10,6 @@ import type { FrameHost, SetPrimaryButton, } from "@farcaster/frame-sdk"; -import type { UseWalletClientReturnType } from "wagmi"; import type { WebView, WebViewProps } from "react-native-webview"; import { z } from "zod"; import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; @@ -19,6 +18,8 @@ import type { ExtractRequest, Default as DefaultRpcSchema } from "ox/RpcSchema"; import { useFreshRef } from "./hooks/use-fresh-ref"; import type { FarcasterSignerState } from "./farcaster"; import type { FarcasterSigner } from "./identity/farcaster"; +import type { EthProvider } from "./frame-app/provider/types"; +import type { FrameClientConfig, FrameEvent } from "./frame-app/types"; export type SendTransactionRpcRequest = ExtractRequest< DefaultRpcSchema, @@ -145,30 +146,63 @@ const defaultOnSignTypedDataRequest: OnSignTypedDataRequestFunction = () => { type UseFrameAppOptions = { /** - * Wallet client from wagmi's useWalletClient() hook + * @example + * ```ts + * import { useWagmiProvider } from '@frames.js/render/frame-app/provider/wagmi'; + * + * function Component() { + * const provider = useWagmiProvider(); + * const frameApp = useFrameApp({ + * provider, + * }); + * + * //... + * } + * ``` + */ + provider: EthProvider; + /** + * Frame client that is rendering the app */ - walletClient: UseWalletClientReturnType; + client: FrameClientConfig; /** * Obtained from useFrame() onLaunchFrameButtonPressed() callback */ frame: ParseFramesV2ResultWithFrameworkDetails; /** * Farcaster signer state. Must be already approved otherwise it will call onError - * and getting context in app will be rejected + * and getting context in frames app will be rejected + * + * @example + * ```ts + * import { useFarcasterSigner } from '@frames.js/render/identity/farcaster'; + * + * function Component() { + * const farcasterSigner = useFarcasterSigner({ + * // ... + * }); + * const frameApp = useFrameApp({ + * farcasterSigner, + * // ... + * }); + * + * //... + * } + * ``` */ farcasterSigner: FarcasterSignerState; /** * Called when app calls `ready` method. */ - onReady?: () => void; + onReady?: FrameHost["ready"]; /** * Called when app calls `close` method. */ - onClose?: () => void; + onClose?: FrameHost["close"]; /** * Called when app calls `openUrl` method. */ - onOpenUrl?: (url: string) => void; + onOpenUrl?: FrameHost["openUrl"]; /** * Called when provided signer is not approved. */ @@ -208,6 +242,9 @@ type RegisterEndpointFunction = ( type UseFrameAppReturn = { /** * Necessary to call with target endpoint to expose API to the frame. + * + * The function returns a function to unregister the endpoint listener. Make sure you call it + * when the frame app is closed. */ registerEndpoint: RegisterEndpointFunction; }; @@ -216,7 +253,8 @@ type UseFrameAppReturn = { * This hook is used to handle frames v2 apps. */ export function useFrameApp({ - walletClient: client, + provider, + client, farcasterSigner, frame, onClose, @@ -231,6 +269,7 @@ export function useFrameApp({ onSignMessageRequest = defaultOnSignMessageRequest, onSignTypedDataRequest = defaultOnSignTypedDataRequest, }: UseFrameAppOptions): UseFrameAppReturn { + const providerRef = useFreshRef(provider); const clientRef = useFreshRef(client); const readyRef = useFreshRef(onReady); const closeRef = useFreshRef(onClose); @@ -260,6 +299,8 @@ export function useFrameApp({ } ); + providerRef.current.emitter.setDebugMode(debug); + const registerEndpoint = useCallback( (endpoint) => { logDebugRef.current( @@ -289,82 +330,50 @@ export function useFrameApp({ }; } + // bridge events to the frame app + const disconnectEndpointFromEvents = + providerRef.current.emitter.forwardToEndpoint(endpoint); + const apiToExpose: FrameHost = { close() { logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "close" called' ); - const handler = closeRef.current; - - if (!handler) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.warn( - '@frames.js/render/unstable-use-frame-app: "close" called but no handler provided' - ); - } else { - handler(); - } + closeRef.current?.(); }, get context() { logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "context" getter called' ); - return { user: { fid: signer.fid } }; + return { user: { fid: signer.fid }, client: clientRef.current }; }, openUrl(url) { logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "openUrl" called', url ); - const handler = onOpenUrlRef.current; - - if (!handler) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.warn( - '@frames.js/render/unstable-use-frame-app: "openUrl" called but no handler provided' - ); - } else { - handler(url); - } + onOpenUrlRef.current?.(url); }, - ready() { + ready(options) { logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "ready" called' ); - const handler = readyRef.current; - - if (!handler) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.warn( - '@frames.js/render/unstable-use-frame-app: "ready" called but no handler provided' - ); - } else { - handler(); - } + readyRef.current?.(options); }, setPrimaryButton(options) { logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "setPrimaryButton" called', options ); - const handler = onPrimaryButtonSetRef.current; - - if (!handler) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.warn( - '@frames.js/render/unstable-use-frame-app: "setPrimaryButton" called but no handler provided' + onPrimaryButtonSetRef.current?.(options, () => { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "primaryButtonClicked" called' ); - } else { - handler(options, () => { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "primaryButtonClicked" called' - ); - endpoint.postMessage({ - type: "frameEvent", - event: "primaryButtonClicked", - }); - }); - } + endpoint.postMessage({ + type: "frameEvent", + event: "primaryButtonClicked", + } satisfies FrameEvent); + }); }, async ethProviderRequest(parameters) { logDebugRef.current( @@ -372,10 +381,6 @@ export function useFrameApp({ parameters ); - if (!clientRef.current.data) { - throw new Error("client is not ready"); - } - let isApproved = true; if (isSendTransactionRpcRequest(parameters)) { @@ -393,7 +398,7 @@ export function useFrameApp({ } // @ts-expect-error -- type mismatch - return clientRef.current.data.request(parameters); + return providerRef.current.request(parameters); }, ethProviderRequestV2(parameters) { logDebugRef.current( @@ -413,7 +418,7 @@ export function useFrameApp({ ) { return { added: false, - reason: "invalid-domain-manifest", + reason: "invalid_domain_manifest", }; } @@ -422,7 +427,7 @@ export function useFrameApp({ ) { return { added: false, - reason: "rejected-by-user", + reason: "rejected_by_user", }; } @@ -442,7 +447,7 @@ export function useFrameApp({ if (!added) { return { added: false, - reason: "rejected-by-user", + reason: "rejected_by_user", }; } @@ -460,20 +465,27 @@ export function useFrameApp({ "@frames.js/render/unstable-use-frame-app: endpoint registered" ); - return unregisterPreviouslyExposedEndpointListenerRef.current; + return () => { + disconnectEndpointFromEvents(); + unregisterPreviouslyExposedEndpointListenerRef.current(); + }; }, [ - addFrameRequestsCacheRef, - clientRef, - closeRef, + logDebugRef, farcasterSignerRef, frame, - logDebugRef, - onAddFrameRequestedRef, - onOpenUrlRef, - onPrimaryButtonSetRef, + providerRef, onSignerNotApprovedRef, + closeRef, + clientRef, + onOpenUrlRef, readyRef, + onPrimaryButtonSetRef, + onSendTransactionRequestRef, + onSignTypedDataRequestRef, + onSignMessageRequestRef, + addFrameRequestsCacheRef, + onAddFrameRequestedRef, ] ); @@ -495,16 +507,16 @@ type UseFrameAppInIframeReturn = { * @example * ``` * import { useFrameAppInIframe } from '@frames.js/render/unstable-use-frame-app'; - * import { useWalletClient } from 'wagmi'; + * import { useWagmiProvider } from '@frames.js/render/frame-app/provider/wagmi'; * import { useFarcasterSigner } from '@frames.js/render/identity/farcaster'; * * function MyAppDialog() { - * const walletClient = useWalletClient(); + * const provider = useWagmiProvider(); * const farcasterSigner = useFarcasterSigner({ * // ... * }); * const frameAppProps = useFrameAppInIframe({ - * walletClient, + * provider, * farcasterSigner, * // frame returned by useFrame() hook * frame: frameState.frame, @@ -595,16 +607,20 @@ const webViewEventParser = z.record(z.any()); * @example * ``` * import { useFrameAppInWebView } from '@frames.js/render/unstable-use-frame-app'; - * import { useWalletClient } from 'wagmi'; + * import { useWagmiProvider } from '@frames.js/render/frame-app/provider/wagmi'; * import { useFarcasterSigner } from '@frames.js/render/identity/farcaster'; * * function MyAppDialog() { + * const provider = useWagmiProvider(); + * const farcasterSigner = useFarcasterSigner({ + * // ... + * }); * const frameAppProps = useFrameAppInWebView({ - * walletClient, - * farcasterSigner, - * // frame returned by useFrame() hook - * frame: frameState.frame, - * // ... handlers for frame app actions + * provider, + * farcasterSigner, + * // frame returned by useFrame() hook + * frame: frameState.frame, + * // ... handlers for frame app actions * }); * * return ; diff --git a/yarn.lock b/yarn.lock index fa16b4e26..77a3c31f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2397,20 +2397,20 @@ neverthrow "^6.0.0" viem "^1.12.2" -"@farcaster/frame-core@^0.0.9": - version "0.0.9" - resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.9.tgz#930e86c86aa772c07fb256cb547dd3e95a1c57d0" - integrity sha512-cZtxQVjRb+2yjJ2zWxr4Ks4Ii5tP1BVVLWqciLe46ItPS4Wp+xX/ItDKcOQq6/t/oHzIKnDjnsfAtN6QGpoU0Q== +"@farcaster/frame-core@^0.0.14": + version "0.0.14" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.14.tgz#4c9ed21be3b4c5961886c47ed58d8a77afc2f082" + integrity sha512-4Zm2IO8N7e1jshryG7zNIKna/ctwtLsamRwrXG8Pd4Parf+2QmCkg7wLb/VuZpHHCqH+Ez34tzggg6cEBEn6NA== dependencies: ox "^0.4.0" zod "^3.23.8" -"@farcaster/frame-sdk@^0.0.10": - version "0.0.10" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.10.tgz#4732f5145f9f2cc24151a5367f43d20da546b168" - integrity sha512-AKRAYw8PeSFfiyuwl64vzOeFdnguH/H631T1Ul3l/x9faugltLlezzkvH0I7Va6dNwOFGaZHGzjajp9R0SyvLg== +"@farcaster/frame-sdk@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.15.tgz#10824faf2832f981fdde1e96366f73bcf4e23b5e" + integrity sha512-gGM8ZGeh8c2JvoUL3FsOowuaTu8v4a6WThEdcPuNA6kxKXto+tzN6uacll/c2vuFbee9Vu/LlOW49HGbAZuRqQ== dependencies: - "@farcaster/frame-core" "^0.0.9" + "@farcaster/frame-core" "^0.0.14" comlink "^4.4.2" eventemitter3 "^5.0.1" ox "^0.4.0" From 05fb8c221a603c70a59f43155ff51efbec9e457f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 14:26:14 +0100 Subject: [PATCH 49/88] chore: use new provider in debugger --- packages/debugger/app/components/frame-app-dialog.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/debugger/app/components/frame-app-dialog.tsx b/packages/debugger/app/components/frame-app-dialog.tsx index 635dc035c..f8c7a55cc 100644 --- a/packages/debugger/app/components/frame-app-dialog.tsx +++ b/packages/debugger/app/components/frame-app-dialog.tsx @@ -9,7 +9,8 @@ import { useState } from "react"; import { useFrameAppInIframe, type FramePrimaryButton, -} from "@frames.js/render/unstable-use-frame-app"; +} from "@frames.js/render/use-frame-app"; +import { useWagmiProvider } from "@frames.js/render/frame-app/provider/wagmi"; import type { LaunchFrameButtonPressEvent } from "@frames.js/render/unstable-types"; import Image from "next/image"; import { cn } from "@/lib/utils"; @@ -39,9 +40,14 @@ export function FrameAppDialog({ const [primaryButton, setPrimaryButton] = useState( null ); + const provider = useWagmiProvider(); const frameApp = useFrameAppInIframe({ debug: true, - walletClient, + client: { + clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), + added: false, + }, + provider, farcasterSigner, frame: frameState.parseResult, onReady() { From 1196efc261a6c12fece31cf88d99302dedc291c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 15:41:41 +0100 Subject: [PATCH 50/88] feat: fetch frame in useFrameApp hook --- .../app/components/frame-app-dialog.tsx | 12 +- packages/render/src/assert-never.ts | 3 + .../src/frame-app/use-fetch-frame-app.ts | 139 ++++ .../render/src/unstable-use-fetch-frame.ts | 7 +- packages/render/src/use-frame-app.ts | 747 ++++++++++-------- 5 files changed, 573 insertions(+), 335 deletions(-) create mode 100644 packages/render/src/assert-never.ts create mode 100644 packages/render/src/frame-app/use-fetch-frame-app.ts diff --git a/packages/debugger/app/components/frame-app-dialog.tsx b/packages/debugger/app/components/frame-app-dialog.tsx index f8c7a55cc..8b1e8278e 100644 --- a/packages/debugger/app/components/frame-app-dialog.tsx +++ b/packages/debugger/app/components/frame-app-dialog.tsx @@ -49,7 +49,8 @@ export function FrameAppDialog({ }, provider, farcasterSigner, - frame: frameState.parseResult, + source: frameState.parseResult, + proxyUrl: "/frames", onReady() { setIsReady(true); }, @@ -59,11 +60,12 @@ export function FrameAppDialog({ }, onPrimaryButtonSet: setPrimaryButton, }); - const { name, url, splashImageUrl, splashBackgroundColor } = + const { name, splashImageUrl, splashBackgroundColor } = frameState.frame.button.action; const isLoadingWallet = walletClient.status === "pending"; - const isLoading = isLoadingWallet || !isReady; + const isLoading = + isLoadingWallet || !isReady || frameApp.status === "pending"; return (
)} - {!isLoadingWallet && ( + {!isLoadingWallet && frameApp.status === "success" && ( )} diff --git a/packages/render/src/assert-never.ts b/packages/render/src/assert-never.ts new file mode 100644 index 000000000..3a0063395 --- /dev/null +++ b/packages/render/src/assert-never.ts @@ -0,0 +1,3 @@ +export function assertNever(x: never): never { + throw new Error(`Unhandled value ${JSON.stringify(x)}`); +} diff --git a/packages/render/src/frame-app/use-fetch-frame-app.ts b/packages/render/src/frame-app/use-fetch-frame-app.ts new file mode 100644 index 000000000..fe1fa5405 --- /dev/null +++ b/packages/render/src/frame-app/use-fetch-frame-app.ts @@ -0,0 +1,139 @@ +import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; +import { useCallback, useEffect, useRef, useState } from "react"; +import { useFreshRef } from "../hooks/use-fresh-ref"; +import { fetchProxied } from "../unstable-use-fetch-frame"; +import { isParseFramesWithReportsResult } from "../helpers"; + +type State = + | { + status: "success"; + frame: ParseFramesV2ResultWithFrameworkDetails; + source: string | URL | ParseFramesV2ResultWithFrameworkDetails; + } + | { + status: "error"; + error: Error; + } + | { + status: "pending"; + }; + +type UseFetchFrameAppOptions = { + source: string | URL | ParseFramesV2ResultWithFrameworkDetails; + proxyUrl: string; + fetchFn?: typeof fetch; +}; + +type UseFetchFrameAppResult = State; + +const defaultFetchFunction: typeof fetch = (...args) => fetch(...args); + +export function useFetchFrameApp({ + fetchFn, + source, + proxyUrl, +}: UseFetchFrameAppOptions): UseFetchFrameAppResult { + const abortControllerRef = useRef(null); + const fetchRef = useFreshRef(fetchFn ?? defaultFetchFunction); + const [state, setState] = useState(() => { + if (typeof source === "string" || source instanceof URL) { + return { + status: "pending", + }; + } + + return { + status: "success", + frame: source, + source, + }; + }); + + const fetchFrame = useCallback( + (sourceUrl: string | URL) => { + // cancel previous request + abortControllerRef.current?.abort(); + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + // we don't want to return promise from fetchFrame because it is used in effect + Promise.resolve(sourceUrl) + .then(async (url) => { + const responseOrError = await fetchProxied({ + fetchFn: fetchRef.current, + url: url.toString(), + parseFarcasterManifest: true, + proxyUrl, + signal: abortController.signal, + }); + + if (responseOrError instanceof Error) { + throw responseOrError; + } + + if (!responseOrError.ok) { + throw new Error( + `Failed to fetch frame, server returned status ${responseOrError.status}` + ); + } + + const data = (await responseOrError.json()) as Promise; + + if (!isParseFramesWithReportsResult(data)) { + throw new Error( + "Invalid response, expected parse result, make sure you are using @frames.js/render proxy" + ); + } + + setState({ + status: "success", + frame: data.farcaster_v2, + source: sourceUrl, + }); + }) + .catch((e) => { + setState({ + status: "error", + error: e instanceof Error ? e : new Error(String(e)), + }); + }); + + try { + setState({ + status: "pending", + }); + } catch (e) { + setState({ + status: "error", + error: e instanceof Error ? e : new Error(String(e)), + }); + } + + return () => { + abortController.abort(); + }; + }, + [fetchRef, proxyUrl] + ); + + useEffect(() => { + if (typeof source === "string" || source instanceof URL) { + return fetchFrame(source); + } + + setState((val) => { + if (val.status === "success" && val.source === source) { + return val; + } + + return { + status: "success", + frame: source, + source, + }; + }); + }, [source, fetchFrame]); + + return state; +} diff --git a/packages/render/src/unstable-use-fetch-frame.ts b/packages/render/src/unstable-use-fetch-frame.ts index c0a91a604..5713aba2e 100644 --- a/packages/render/src/unstable-use-fetch-frame.ts +++ b/packages/render/src/unstable-use-fetch-frame.ts @@ -806,6 +806,7 @@ type FetchProxiedArg = { * Valid only for GET requests */ parseFarcasterManifest?: boolean; + signal?: AbortSignal; } & ( | { frameAction: SignedFrameAction; @@ -814,7 +815,7 @@ type FetchProxiedArg = { | { url: string } ); -async function fetchProxied( +export async function fetchProxied( params: FetchProxiedArg ): Promise { const searchParams = new URLSearchParams({ @@ -850,7 +851,9 @@ async function fetchProxied( const proxyUrl = proxyUrlAndSearchParamsToUrl(params.proxyUrl, searchParams); - return tryCallAsync(() => params.fetchFn(proxyUrl, { method: "GET" })); + return tryCallAsync(() => + params.fetchFn(proxyUrl, { method: "GET", signal: params.signal }) + ); } function getResponseBody(response: Response): Promise { diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index f3a405d6e..99b398195 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -3,7 +3,7 @@ import { windowEndpoint, type Endpoint, } from "@michalkvasnicak/comlink"; -import { type LegacyRef, useCallback, useEffect, useMemo, useRef } from "react"; +import { type LegacyRef, useEffect, useMemo, useRef } from "react"; import type { AddFrameResult, EthProviderRequest, @@ -20,6 +20,8 @@ import type { FarcasterSignerState } from "./farcaster"; import type { FarcasterSigner } from "./identity/farcaster"; import type { EthProvider } from "./frame-app/provider/types"; import type { FrameClientConfig, FrameEvent } from "./frame-app/types"; +import { assertNever } from "./assert-never"; +import { useFetchFrameApp } from "./frame-app/use-fetch-frame-app"; export type SendTransactionRpcRequest = ExtractRequest< DefaultRpcSchema, @@ -166,9 +168,24 @@ type UseFrameAppOptions = { */ client: FrameClientConfig; /** - * Obtained from useFrame() onLaunchFrameButtonPressed() callback + * Either: + * + * - frame parse result obtained from useFrame() hook + * - frame url as string or URL object + * + * If value changes it resets the internal state and refetches the frame if the new value is url. + */ + source: ParseFramesV2ResultWithFrameworkDetails | string | URL; + /** + * Custom fetch compatible function used to make requests. + * + * @see https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API + */ + fetchFn?: typeof fetch; + /** + * Frame proxyUrl used to fetch the frame parse result. */ - frame: ParseFramesV2ResultWithFrameworkDetails; + proxyUrl: string; /** * Farcaster signer state. Must be already approved otherwise it will call onError * and getting context in frames app will be rejected @@ -239,15 +256,25 @@ type RegisterEndpointFunction = ( endpoint: Endpoint ) => UnregisterEndpointFunction; -type UseFrameAppReturn = { - /** - * Necessary to call with target endpoint to expose API to the frame. - * - * The function returns a function to unregister the endpoint listener. Make sure you call it - * when the frame app is closed. - */ - registerEndpoint: RegisterEndpointFunction; -}; +type UseFrameAppReturn = + | { + /** + * Necessary to call with target endpoint to expose API to the frame. + * + * The function returns a function to unregister the endpoint listener. Make sure you call it + * when the frame app is closed. + */ + registerEndpoint: RegisterEndpointFunction; + frame: ParseFramesV2ResultWithFrameworkDetails; + status: "success"; + } + | { + status: "pending"; + } + | { + error: Error; + status: "error"; + }; /** * This hook is used to handle frames v2 apps. @@ -256,7 +283,9 @@ export function useFrameApp({ provider, client, farcasterSigner, - frame, + source, + fetchFn, + proxyUrl, onClose, onOpenUrl, onPrimaryButtonSet, @@ -282,6 +311,11 @@ export function useFrameApp({ const onSignMessageRequestRef = useFreshRef(onSignMessageRequest); const onSignTypedDataRequestRef = useFreshRef(onSignTypedDataRequest); const addFrameRequestsCacheRef = useFreshRef(addFrameRequestsCache); + const frameResolutionState = useFetchFrameApp({ + source, + fetchFn, + proxyUrl, + }); /** * Used to unregister message listener of previously exposed endpoint. */ @@ -301,203 +335,232 @@ export function useFrameApp({ providerRef.current.emitter.setDebugMode(debug); - const registerEndpoint = useCallback( - (endpoint) => { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: registering endpoint" - ); - unregisterPreviouslyExposedEndpointListenerRef.current(); - - const signer = farcasterSignerRef.current.signer; - - if (signer?.status !== "approved") { - onSignerNotApprovedRef.current(); - - return () => { - // no-op - }; - } - - const frameUrl = frame.frame.button?.action?.url; - - if (!frameUrl) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.error( - '@frames.js/render/unstable-use-frame-app: provided "frame" does not have an action url' - ); - return () => { - // no-op - }; - } + return useMemo(() => { + switch (frameResolutionState.status) { + case "success": { + const frame = frameResolutionState.frame; - // bridge events to the frame app - const disconnectEndpointFromEvents = - providerRef.current.emitter.forwardToEndpoint(endpoint); - - const apiToExpose: FrameHost = { - close() { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "close" called' - ); - closeRef.current?.(); - }, - get context() { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "context" getter called' - ); - return { user: { fid: signer.fid }, client: clientRef.current }; - }, - openUrl(url) { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "openUrl" called', - url - ); - onOpenUrlRef.current?.(url); - }, - ready(options) { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "ready" called' - ); - readyRef.current?.(options); - }, - setPrimaryButton(options) { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "setPrimaryButton" called', - options - ); - onPrimaryButtonSetRef.current?.(options, () => { + return { + registerEndpoint(endpoint) { logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "primaryButtonClicked" called' + "@frames.js/render/unstable-use-frame-app: registering endpoint" ); - endpoint.postMessage({ - type: "frameEvent", - event: "primaryButtonClicked", - } satisfies FrameEvent); - }); - }, - async ethProviderRequest(parameters) { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "ethProviderRequest" called', - parameters - ); - - let isApproved = true; - - if (isSendTransactionRpcRequest(parameters)) { - isApproved = await onSendTransactionRequestRef.current(parameters); - } else if (isSignTypedDataRpcRequest(parameters)) { - isApproved = await onSignTypedDataRequestRef.current(parameters); - } else if (isSignMessageRpcRequest(parameters)) { - isApproved = await onSignMessageRequestRef.current(parameters); - } + unregisterPreviouslyExposedEndpointListenerRef.current(); - if (!isApproved) { - throw new UserRejectedRequestError( - new Error("User rejected request") - ); - } + const signer = farcasterSignerRef.current.signer; - // @ts-expect-error -- type mismatch - return providerRef.current.request(parameters); - }, - ethProviderRequestV2(parameters) { - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "ethProviderRequestV2" called', - parameters - ); - - // this is stupid because previously it was enough just to not expose this method at all - // but now it suddenly stopped working because somehow the error message was not the same - // as @farcasters/frame-sdk was expecting - return Promise.reject(new Error("cannot read property 'apply'")); - }, - async addFrame() { - if ( - frame.status !== "success" || - frame.manifest?.status !== "success" - ) { - return { - added: false, - reason: "invalid_domain_manifest", - }; - } + if (signer?.status !== "approved") { + onSignerNotApprovedRef.current(); - if ( - addFrameRequestsCacheRef.current.has(frame.frame.button.action.url) - ) { - return { - added: false, - reason: "rejected_by_user", - }; - } - - logDebugRef.current( - '@frames.js/render/unstable-use-frame-app: "addFrame" called' - ); + return () => { + // no-op + }; + } - const added = await onAddFrameRequestedRef.current(frame); + const frameUrl = frame.frame.button?.action?.url; - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: addFrameRequested", - added - ); + if (!frameUrl) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.error( + '@frames.js/render/unstable-use-frame-app: provided "frame" does not have an action url' + ); + return () => { + // no-op + }; + } - addFrameRequestsCacheRef.current.add(frame.frame.button.action.url); + // bridge events to the frame app + const disconnectEndpointFromEvents = + providerRef.current.emitter.forwardToEndpoint(endpoint); - if (!added) { - return { - added: false, - reason: "rejected_by_user", + const apiToExpose: FrameHost = { + close() { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "close" called' + ); + closeRef.current?.(); + }, + get context() { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "context" getter called' + ); + return { user: { fid: signer.fid }, client: clientRef.current }; + }, + openUrl(url) { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "openUrl" called', + url + ); + onOpenUrlRef.current?.(url); + }, + ready(options) { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "ready" called' + ); + readyRef.current?.(options); + }, + setPrimaryButton(options) { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "setPrimaryButton" called', + options + ); + onPrimaryButtonSetRef.current?.(options, () => { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "primaryButtonClicked" called' + ); + endpoint.postMessage({ + type: "frameEvent", + event: "primaryButtonClicked", + } satisfies FrameEvent); + }); + }, + async ethProviderRequest(parameters) { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "ethProviderRequest" called', + parameters + ); + + let isApproved = true; + + if (isSendTransactionRpcRequest(parameters)) { + isApproved = + await onSendTransactionRequestRef.current(parameters); + } else if (isSignTypedDataRpcRequest(parameters)) { + isApproved = + await onSignTypedDataRequestRef.current(parameters); + } else if (isSignMessageRpcRequest(parameters)) { + isApproved = + await onSignMessageRequestRef.current(parameters); + } + + if (!isApproved) { + throw new UserRejectedRequestError( + new Error("User rejected request") + ); + } + + // @ts-expect-error -- type mismatch + return providerRef.current.request(parameters); + }, + ethProviderRequestV2(parameters) { + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "ethProviderRequestV2" called', + parameters + ); + + // this is stupid because previously it was enough just to not expose this method at all + // but now it suddenly stopped working because somehow the error message was not the same + // as @farcasters/frame-sdk was expecting + return Promise.reject( + new Error("cannot read property 'apply'") + ); + }, + async addFrame() { + if ( + frame.status !== "success" || + frame.manifest?.status !== "success" + ) { + return { + added: false, + reason: "invalid_domain_manifest", + }; + } + + if ( + addFrameRequestsCacheRef.current.has( + frame.frame.button.action.url + ) + ) { + return { + added: false, + reason: "rejected_by_user", + }; + } + + logDebugRef.current( + '@frames.js/render/unstable-use-frame-app: "addFrame" called' + ); + + const added = await onAddFrameRequestedRef.current(frame); + + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: addFrameRequested", + added + ); + + addFrameRequestsCacheRef.current.add( + frame.frame.button.action.url + ); + + if (!added) { + return { + added: false, + reason: "rejected_by_user", + }; + } + + return added; + }, }; - } - return added; - }, - }; - - unregisterPreviouslyExposedEndpointListenerRef.current = expose( - apiToExpose, - endpoint, - [new URL(frameUrl).origin] - ); - - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: endpoint registered" - ); - - return () => { - disconnectEndpointFromEvents(); - unregisterPreviouslyExposedEndpointListenerRef.current(); - }; - }, - [ - logDebugRef, - farcasterSignerRef, - frame, - providerRef, - onSignerNotApprovedRef, - closeRef, - clientRef, - onOpenUrlRef, - readyRef, - onPrimaryButtonSetRef, - onSendTransactionRequestRef, - onSignTypedDataRequestRef, - onSignMessageRequestRef, - addFrameRequestsCacheRef, - onAddFrameRequestedRef, - ] - ); + unregisterPreviouslyExposedEndpointListenerRef.current = expose( + apiToExpose, + endpoint, + [new URL(frameUrl).origin] + ); + + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: endpoint registered" + ); - return useMemo(() => { - return { registerEndpoint }; - }, [registerEndpoint]); + return () => { + disconnectEndpointFromEvents(); + unregisterPreviouslyExposedEndpointListenerRef.current(); + }; + }, + status: "success", + frame: frameResolutionState.frame, + }; + } + case "error": { + return { + status: "error", + error: frameResolutionState.error, + }; + } + case "pending": { + return { + status: "pending", + }; + } + default: + assertNever(frameResolutionState); + } + }, [ + frameResolutionState, + logDebugRef, + farcasterSignerRef, + providerRef, + onSignerNotApprovedRef, + closeRef, + clientRef, + onOpenUrlRef, + readyRef, + onPrimaryButtonSetRef, + onSendTransactionRequestRef, + onSignTypedDataRequestRef, + onSignMessageRequestRef, + addFrameRequestsCacheRef, + onAddFrameRequestedRef, + ]); } -type UseFrameAppInIframeReturn = { - onLoad: (event: React.SyntheticEvent) => void; - src: string | undefined; -}; +type UseFrameAppInIframeReturn = + | Exclude + | ({ + onLoad: (event: React.SyntheticEvent) => void; + src: string | undefined; + } & Extract); /** * Handles frame app in iframe. @@ -553,32 +616,43 @@ export function useFrameAppInIframe( }; }, [logDebugRef]); - return useMemo(() => { - return { - onLoad(event) { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: iframe loaded" - ); + return useMemo(() => { + switch (frameApp.status) { + case "error": + case "pending": + return frameApp; + case "success": { + return { + ...frameApp, + status: "success", + src: frameApp.frame.frame.button?.action?.url, + onLoad(event) { + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: iframe loaded" + ); - if (!(event.currentTarget instanceof HTMLIFrameElement)) { - // eslint-disable-next-line no-console -- provide feedback to the developer - console.error( - '@frames.js/render/unstable-use-frame-app: "onLoad" called but event target is not an iframe' - ); + if (!(event.currentTarget instanceof HTMLIFrameElement)) { + // eslint-disable-next-line no-console -- provide feedback to the developer + console.error( + '@frames.js/render/unstable-use-frame-app: "onLoad" called but event target is not an iframe' + ); - return; - } - if (!event.currentTarget.contentWindow) { - return; - } + return; + } + if (!event.currentTarget.contentWindow) { + return; + } - const endpoint = windowEndpoint(event.currentTarget.contentWindow); + const endpoint = windowEndpoint(event.currentTarget.contentWindow); - unregisterEndpointRef.current = frameApp.registerEndpoint(endpoint); - }, - src: options.frame.frame.button?.action?.url, - }; - }, [frameApp, logDebugRef, options.frame]); + unregisterEndpointRef.current = frameApp.registerEndpoint(endpoint); + }, + }; + } + default: + assertNever(frameApp); + } + }, [frameApp, logDebugRef]); } type ReactNativeMessageEvent = { @@ -588,14 +662,16 @@ type ReactNativeMessageEvent = { type MessageEventListener = (event: ReactNativeMessageEvent) => void; -type UseFrameAppInWebViewReturn = { - source: WebViewProps["source"]; - onMessage: NonNullable; - injectedJavaScriptBeforeContentLoaded: NonNullable< - WebViewProps["injectedJavaScriptBeforeContentLoaded"] - >; - ref: LegacyRef; -}; +type UseFrameAppInWebViewReturn = + | Exclude + | (Extract & { + source: WebViewProps["source"]; + onMessage: NonNullable; + injectedJavaScriptBeforeContentLoaded: NonNullable< + WebViewProps["injectedJavaScriptBeforeContentLoaded"] + >; + ref: LegacyRef; + }); const webViewEventParser = z.record(z.any()); @@ -631,6 +707,7 @@ export function useFrameAppInWebView( options: UseFrameAppOptions ): UseFrameAppInWebViewReturn { const ref = useRef(null); + const debugRef = useFreshRef(options.debug ?? false); const frameApp = useFrameApp(options); const unregisterEndpointRef = useRef(() => { // no-op @@ -661,115 +738,129 @@ export function useFrameAppInWebView( }; }, [logDebugRef]); - const frameUrl = options.frame.frame.button?.action?.url; - - return { - source: frameUrl ? { uri: frameUrl } : undefined, - onMessage(event) { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: received an event", - event.nativeEvent.data - ); - - try { - const result = z - .preprocess((value) => { - return typeof value === "string" ? JSON.parse(value) : value; - }, webViewEventParser) - .safeParse(event.nativeEvent.data); - - if (!result.success) { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: received event parsing error", - result.error - ); - return; - } - - const messageEvent = { - origin: new URL(event.nativeEvent.url).origin, - data: result.data, - }; - - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: received message from web view", - messageEvent - ); - - messageListenersRef.current?.forEach((listener) => { - listener(messageEvent); - }); - } catch (error) { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: event receiving error", - error - ); - } - }, - // inject js code which handles message parsing between the frame app and the application. - // react native web view is able to send only string messages so we need to serialize/deserialize them - injectedJavaScriptBeforeContentLoaded: createMessageBridgeScript( - options.debug ?? false - ), - ref(webView) { - ref.current = webView; - - if (!webView) { - return; - } - - unregisterEndpointRef.current = frameApp.registerEndpoint({ - postMessage(message) { - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: sent message to web view", - message - ); - - webView.postMessage(JSON.stringify(message)); - }, - addEventListener(type, listener) { - if (type !== "message") { - throw new Error("Invalid event"); - } - - if (typeof listener === "function") { - messageListenersRef.current?.add( - listener as unknown as MessageEventListener - ); - + return useMemo(() => { + switch (frameApp.status) { + case "error": + case "pending": + return frameApp; + case "success": { + const frame = frameApp.frame.frame; + + return { + ...frameApp, + source: frame.button?.action?.url + ? { uri: frame.button.action.url } + : undefined, + onMessage(event) { logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: registered an event listener", - listener + "@frames.js/render/unstable-use-frame-app: received an event", + event.nativeEvent.data ); - } else { - throw new Error('Invalid listener, expected "function"'); - } - }, - removeEventListener(type, listener) { - if (type !== "message") { - throw new Error("Invalid event"); - } - if (typeof listener === "function") { - messageListenersRef.current?.delete( - listener as unknown as MessageEventListener - ); + try { + const result = z + .preprocess((value) => { + return typeof value === "string" ? JSON.parse(value) : value; + }, webViewEventParser) + .safeParse(event.nativeEvent.data); + + if (!result.success) { + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: received event parsing error", + result.error + ); + return; + } + + const messageEvent = { + origin: new URL(event.nativeEvent.url).origin, + data: result.data, + }; + + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: received message from web view", + messageEvent + ); + + messageListenersRef.current?.forEach((listener) => { + listener(messageEvent); + }); + } catch (error) { + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: event receiving error", + error + ); + } + }, + // inject js code which handles message parsing between the frame app and the application. + // react native web view is able to send only string messages so we need to serialize/deserialize them + injectedJavaScriptBeforeContentLoaded: createMessageBridgeScript( + debugRef.current + ), + ref(webView) { + ref.current = webView; + + if (!webView) { + return; + } + + unregisterEndpointRef.current = frameApp.registerEndpoint({ + postMessage(message) { + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: sent message to web view", + message + ); + + webView.postMessage(JSON.stringify(message)); + }, + addEventListener(type, listener) { + if (type !== "message") { + throw new Error("Invalid event"); + } + + if (typeof listener === "function") { + messageListenersRef.current?.add( + listener as unknown as MessageEventListener + ); + + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: registered an event listener", + listener + ); + } else { + throw new Error('Invalid listener, expected "function"'); + } + }, + removeEventListener(type, listener) { + if (type !== "message") { + throw new Error("Invalid event"); + } + + if (typeof listener === "function") { + messageListenersRef.current?.delete( + listener as unknown as MessageEventListener + ); + + logDebugRef.current( + "@frames.js/render/unstable-use-frame-app: removed an event listener", + listener + ); + } else { + throw new Error('Invalid listener, expected "function"'); + } + }, + }); logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: removed an event listener", - listener + "@frames.js/render/unstable-use-frame-app: registered web view endpoint" ); - } else { - throw new Error('Invalid listener, expected "function"'); - } - }, - }); - - logDebugRef.current( - "@frames.js/render/unstable-use-frame-app: registered web view endpoint" - ); - }, - }; + }, + }; + } + default: + assertNever(frameApp); + } + }, [frameApp, logDebugRef, debugRef]); } function createMessageBridgeScript(enableDebug: boolean): string { From eaeafe48bbfdbae6a4e858ddc80c75c1443b0aa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 15:51:35 +0100 Subject: [PATCH 51/88] fix: peer dep --- packages/render/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/package.json b/packages/render/package.json index 5af53f32c..250e842ac 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -332,7 +332,7 @@ }, "license": "MIT", "peerDependencies": { - "@farcaster/frame-sdk": "^0.0.11", + "@farcaster/frame-sdk": "^0.0.15", "@lens-protocol/client": "^2.0.0", "@rainbow-me/rainbowkit": "^2.1.2", "@types/react": "^18.2.0", From 36e5d12696d84f2c0a8d2c10a1593fd9c20a73df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 15:56:28 +0100 Subject: [PATCH 52/88] chore: return props as object --- packages/render/src/use-frame-app.ts | 269 +++++++++++++++------------ 1 file changed, 146 insertions(+), 123 deletions(-) diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index 99b398195..0964e5bc4 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -558,8 +558,10 @@ export function useFrameApp({ type UseFrameAppInIframeReturn = | Exclude | ({ - onLoad: (event: React.SyntheticEvent) => void; - src: string | undefined; + iframeProps: { + onLoad: (event: React.SyntheticEvent) => void; + src: string | undefined; + }; } & Extract); /** @@ -578,7 +580,7 @@ type UseFrameAppInIframeReturn = * const farcasterSigner = useFarcasterSigner({ * // ... * }); - * const frameAppProps = useFrameAppInIframe({ + * const frameApp = useFrameAppInIframe({ * provider, * farcasterSigner, * // frame returned by useFrame() hook @@ -586,7 +588,12 @@ type UseFrameAppInIframeReturn = * // ... handlers for frame app actions * }); * - * return )}
From c58f502277f2d4b71df9282909a030f2f7f337ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 11 Dec 2024 16:04:42 +0100 Subject: [PATCH 54/88] fix: eslint issue --- packages/render/src/frame-app/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/render/src/frame-app/types.ts b/packages/render/src/frame-app/types.ts index 946f4f644..a84d800a6 100644 --- a/packages/render/src/frame-app/types.ts +++ b/packages/render/src/frame-app/types.ts @@ -10,7 +10,7 @@ export type FrameEvent = { }; /** - * This is here just because it is incosistent in @farcaster/frame-sdk + * This is here just because it is inconsistent in farcaster/frame-sdk * Eventually this will be removed if they fix that. * * This is just a type that is used as a lead how to convert FrameEvent to FrameEventReactNative From b1428251ecbbb70dfbb708a3433626409ef63b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 12 Dec 2024 09:16:48 +0100 Subject: [PATCH 55/88] chore: export return and option types --- packages/render/src/use-frame-app.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index 0964e5bc4..711160c81 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -146,7 +146,7 @@ const defaultOnSignTypedDataRequest: OnSignTypedDataRequestFunction = () => { return Promise.resolve(true); }; -type UseFrameAppOptions = { +export type UseFrameAppOptions = { /** * @example * ```ts @@ -256,7 +256,7 @@ type RegisterEndpointFunction = ( endpoint: Endpoint ) => UnregisterEndpointFunction; -type UseFrameAppReturn = +export type UseFrameAppReturn = | { /** * Necessary to call with target endpoint to expose API to the frame. @@ -555,7 +555,7 @@ export function useFrameApp({ ]); } -type UseFrameAppInIframeReturn = +export type UseFrameAppInIframeReturn = | Exclude | ({ iframeProps: { @@ -674,7 +674,7 @@ type ReactNativeMessageEvent = { type MessageEventListener = (event: ReactNativeMessageEvent) => void; -type UseFrameAppInWebViewReturn = +export type UseFrameAppInWebViewReturn = | Exclude | (Extract & { webViewProps: { From a3f9787321c6942da11e6f061d255a7f597c33e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 12 Dec 2024 15:23:00 +0100 Subject: [PATCH 56/88] fix: check if emitter is really set --- .../render/src/frame-app/provider/wagmi.ts | 43 ++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/packages/render/src/frame-app/provider/wagmi.ts b/packages/render/src/frame-app/provider/wagmi.ts index b61208ac8..a9b10bc87 100644 --- a/packages/render/src/frame-app/provider/wagmi.ts +++ b/packages/render/src/frame-app/provider/wagmi.ts @@ -5,17 +5,58 @@ import { useFreshRef } from "../../hooks/use-fresh-ref"; import type { EthProvider, EthProviderEventEmitterInterface } from "./types"; import { createEmitter } from "./event-emitter"; -export function useWagmiProvider(): EthProvider { +export type UseWagmiProviderOptions = { + /** + * @defaultValue false + */ + debug?: boolean; +}; + +export function useWagmiProvider({ + debug = false, +}: UseWagmiProviderOptions = {}): EthProvider { const account = useAccount(); const config = useConfig(); const configRef = useFreshRef(config); const providerRef = useRef(null); const emitterRef = useRef(null); + const logDebugRef = useFreshRef( + debug + ? // eslint-disable-next-line no-console -- provide feedback to the developer + console.debug + : () => { + // noop + } + ); + useEffect(() => { const connector = account.connector; if (!connector) { + logDebugRef.current( + "@frames.js/render/frame-app/provider/wagmi: No connector found, skipping event listeners registration" + ); + + return; + } + + /** + * This is weird case but sometimes when account is connecting + * injected connector could miss the emitter property. + * + * Not sure if it is because it is connecting or something else + * and the type here is saying that emitter is never nullable + * but we actually ran into the case where it was null. + * + * So just to be sure. + */ + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- it actually can be nullable in some cases + if (!connector.emitter) { + logDebugRef.current( + "@frames.js/render/frame-app/provider/wagmi: No emitter found on connector, skipping event listeners registration" + ); + return; } From 044734a1158a06765c799cf66fd46ff86beef46d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 12 Dec 2024 15:35:31 +0100 Subject: [PATCH 57/88] chore: update peer deps --- packages/render/package.json | 4 ++-- yarn.lock | 18 +++++++++--------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/render/package.json b/packages/render/package.json index 250e842ac..e2fae6eee 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -316,7 +316,7 @@ "src" ], "devDependencies": { - "@farcaster/frame-sdk": "^0.0.15", + "@farcaster/frame-sdk": "^0.0.16", "@lens-protocol/client": "^2.3.2", "@rainbow-me/rainbowkit": "^2.1.2", "@remix-run/node": "^2.8.1", @@ -332,7 +332,7 @@ }, "license": "MIT", "peerDependencies": { - "@farcaster/frame-sdk": "^0.0.15", + "@farcaster/frame-sdk": "^0.0.16", "@lens-protocol/client": "^2.0.0", "@rainbow-me/rainbowkit": "^2.1.2", "@types/react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 77a3c31f6..c91dc6c99 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2397,20 +2397,20 @@ neverthrow "^6.0.0" viem "^1.12.2" -"@farcaster/frame-core@^0.0.14": - version "0.0.14" - resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.14.tgz#4c9ed21be3b4c5961886c47ed58d8a77afc2f082" - integrity sha512-4Zm2IO8N7e1jshryG7zNIKna/ctwtLsamRwrXG8Pd4Parf+2QmCkg7wLb/VuZpHHCqH+Ez34tzggg6cEBEn6NA== +"@farcaster/frame-core@^0.0.15": + version "0.0.15" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.15.tgz#05a4ed6f7c0d43d2f41d13714fb8b13419068a11" + integrity sha512-WQfAEqyQAz3EzEdfqAMV7s2VMIYBGWz0Qt5CUUkmSelvv0a+8A61YmBnpemCi3NEwWzEJBTc/IxzQ29w2axPBg== dependencies: ox "^0.4.0" zod "^3.23.8" -"@farcaster/frame-sdk@^0.0.15": - version "0.0.15" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.15.tgz#10824faf2832f981fdde1e96366f73bcf4e23b5e" - integrity sha512-gGM8ZGeh8c2JvoUL3FsOowuaTu8v4a6WThEdcPuNA6kxKXto+tzN6uacll/c2vuFbee9Vu/LlOW49HGbAZuRqQ== +"@farcaster/frame-sdk@^0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.16.tgz#1e3dc191f950065d2f38daf61dc45775d7073421" + integrity sha512-043j2EiCOaHvS1ox0yVx7KZQU5LB1P+39waK3xtX0knNKQ54wZxCOTcca0kUcJfTBN5db7iVPOh/E+LJ5kjDew== dependencies: - "@farcaster/frame-core" "^0.0.14" + "@farcaster/frame-core" "^0.0.15" comlink "^4.4.2" eventemitter3 "^5.0.1" ox "^0.4.0" From 8c179ae5d3a5489edd1f28c8bc823169f8c7bafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 09:53:42 +0100 Subject: [PATCH 58/88] fix: frame app dialog layout --- .../app/components/frame-app-dialog.tsx | 63 +++++++++++-------- 1 file changed, 37 insertions(+), 26 deletions(-) diff --git a/packages/debugger/app/components/frame-app-dialog.tsx b/packages/debugger/app/components/frame-app-dialog.tsx index 2a91ad159..bf8824355 100644 --- a/packages/debugger/app/components/frame-app-dialog.tsx +++ b/packages/debugger/app/components/frame-app-dialog.tsx @@ -37,9 +37,10 @@ export function FrameAppDialog({ const { toast } = useToast(); const walletClient = useWalletClient(); const [isReady, setIsReady] = useState(false); - const [primaryButton, setPrimaryButton] = useState( - null - ); + const [primaryButton, setPrimaryButton] = useState<{ + button: FramePrimaryButton; + callback: () => void; + } | null>(null); const provider = useWagmiProvider(); const frameApp = useFrameAppInIframe({ debug: true, @@ -58,7 +59,14 @@ export function FrameAppDialog({ onOpenUrl(url) { window.open(url, "_blank"); }, - onPrimaryButtonSet: setPrimaryButton, + onPrimaryButtonSet(button, buttonCallback) { + setPrimaryButton({ + button, + callback: () => { + buttonCallback(); + }, + }); + }, }); const { name, splashImageUrl, splashBackgroundColor } = frameState.frame.button.action; @@ -76,11 +84,11 @@ export function FrameAppDialog({ } }} > - + {frameState.frame.button.action.name} -
+
{isLoading && (
)}
- {primaryButton && !primaryButton.hidden && ( - - - - )} + {!!primaryButton && + primaryButton.button && + !primaryButton.button.hidden && ( + + + + )} ); From 46fff8a009c12ea92dbf20502c9cf7964137532c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 12:07:19 +0100 Subject: [PATCH 59/88] chore: remove unnecessary handler --- packages/render/src/unstable-types.ts | 12 ------------ packages/render/src/unstable-use-frame.tsx | 8 -------- 2 files changed, 20 deletions(-) diff --git a/packages/render/src/unstable-types.ts b/packages/render/src/unstable-types.ts index 0b27ca53b..a65d7c1c4 100644 --- a/packages/render/src/unstable-types.ts +++ b/packages/render/src/unstable-types.ts @@ -200,12 +200,6 @@ export type UseFrameOptions< * Only for frames v2 */ onLaunchFrameButtonPressed?: LaunchFrameButtonPressFunction; - /** - * This function is called when lauched frame is closed. - * - * Only for frames v2 - */ - onLaunchedFrameClosed?: FrameCloseFunction; } & Partial< Pick< UseFetchFrameOptions, @@ -349,12 +343,6 @@ export type UseFrameReturnValue< * Only for frames v2 */ onLaunchFrameButtonPress: LaunchFrameButtonPressFunction; - /** - * Called by UI when the launched frame is closed. - * - * Only for frames v2 - */ - onLaunchedFrameClose: FrameCloseFunction; readonly homeframeUrl: string | null | undefined; /** * Resets the frame state to initial frame and resolves specification and signer again diff --git a/packages/render/src/unstable-use-frame.tsx b/packages/render/src/unstable-use-frame.tsx index a463fb895..232ba956f 100644 --- a/packages/render/src/unstable-use-frame.tsx +++ b/packages/render/src/unstable-use-frame.tsx @@ -180,7 +180,6 @@ export function useFrame_unstable< onRedirect = handleRedirectFallback, fetchFn = defaultFetchFunction, onLaunchFrameButtonPressed, - onLaunchedFrameClosed, onTransactionDataError, onTransactionDataStart, onTransactionDataSuccess, @@ -416,7 +415,6 @@ export function useFrame_unstable< ); const onLaunchFrameButtonPressRef = useFreshRef(onLaunchFrameButtonPressed); - const onLaunchedFrameCloseRef = useFreshRef(onLaunchedFrameClosed); const onLaunchFrameButtonPress = useCallback( (event: LaunchFrameButtonPressEvent) => { @@ -425,10 +423,6 @@ export function useFrame_unstable< [onLaunchFrameButtonPressRef] ); - const onLaunchedFrameClose = useCallback(() => { - onLaunchedFrameCloseRef.current?.(); - }, [onLaunchedFrameCloseRef]); - const onButtonPress = useCallback( async function onButtonPress( currentFrame: Frame, @@ -555,7 +549,6 @@ export function useFrame_unstable< framesStack: stack, currentFrameStackItem: stack[0], onLaunchFrameButtonPress, - onLaunchedFrameClose, }; }, [ signerState, @@ -569,6 +562,5 @@ export function useFrame_unstable< homeframeUrl, stack, onLaunchFrameButtonPress, - onLaunchedFrameClose, ]); } From c8bb17a934eba6cdaf701ea17e587ed9d4b7ef00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 12:50:27 +0100 Subject: [PATCH 60/88] chore: allow to debug eth provider requests --- packages/render/src/use-frame-app.ts | 36 +++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index 711160c81..fc48238c6 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -8,6 +8,8 @@ import type { AddFrameResult, EthProviderRequest, FrameHost, + FrameLocationContext, + FrameLocationContextLauncher, SetPrimaryButton, } from "@farcaster/frame-sdk"; import type { WebView, WebViewProps } from "react-native-webview"; @@ -167,6 +169,12 @@ export type UseFrameAppOptions = { * Frame client that is rendering the app */ client: FrameClientConfig; + /** + * Information about the context from which the frame was launched. + * + * @defaultValue launcher context + */ + location?: FrameLocationContext; /** * Either: * @@ -248,6 +256,12 @@ export type UseFrameAppOptions = { onSendTransactionRequest?: OnSendTransactionRequestFunction; onSignMessageRequest?: OnSignMessageRequestFunction; onSignTypedDataRequest?: OnSignTypedDataRequestFunction; + /** + * Called when the frame app requests eth provider request. + * + * This is called only if debug mode is enabled and should be used only for debugging. + */ + onDebugEthProviderRequest?: (...args: Parameters) => void; }; type UnregisterEndpointFunction = () => void; @@ -276,12 +290,17 @@ export type UseFrameAppReturn = status: "error"; }; +const defaultLocation: FrameLocationContextLauncher = { + type: "launcher", +}; + /** * This hook is used to handle frames v2 apps. */ export function useFrameApp({ provider, client, + location = defaultLocation, farcasterSigner, source, fetchFn, @@ -297,9 +316,12 @@ export function useFrameApp({ onSendTransactionRequest = defaultOnSendTransactionRequest, onSignMessageRequest = defaultOnSignMessageRequest, onSignTypedDataRequest = defaultOnSignTypedDataRequest, + onDebugEthProviderRequest, }: UseFrameAppOptions): UseFrameAppReturn { const providerRef = useFreshRef(provider); + const debugRef = useFreshRef(debug); const clientRef = useFreshRef(client); + const locationRef = useFreshRef(location); const readyRef = useFreshRef(onReady); const closeRef = useFreshRef(onClose); const onOpenUrlRef = useFreshRef(onOpenUrl); @@ -311,6 +333,7 @@ export function useFrameApp({ const onSignMessageRequestRef = useFreshRef(onSignMessageRequest); const onSignTypedDataRequestRef = useFreshRef(onSignTypedDataRequest); const addFrameRequestsCacheRef = useFreshRef(addFrameRequestsCache); + const onDebugEthProviderRequestRef = useFreshRef(onDebugEthProviderRequest); const frameResolutionState = useFetchFrameApp({ source, fetchFn, @@ -384,7 +407,11 @@ export function useFrameApp({ logDebugRef.current( '@frames.js/render/unstable-use-frame-app: "context" getter called' ); - return { user: { fid: signer.fid }, client: clientRef.current }; + return { + user: { fid: signer.fid }, + client: clientRef.current, + location: locationRef.current, + }; }, openUrl(url) { logDebugRef.current( @@ -420,6 +447,10 @@ export function useFrameApp({ parameters ); + if (debugRef.current) { + onDebugEthProviderRequestRef.current?.(parameters); + } + let isApproved = true; if (isSendTransactionRpcRequest(parameters)) { @@ -544,9 +575,12 @@ export function useFrameApp({ onSignerNotApprovedRef, closeRef, clientRef, + locationRef, onOpenUrlRef, readyRef, onPrimaryButtonSetRef, + debugRef, + onDebugEthProviderRequestRef, onSendTransactionRequestRef, onSignTypedDataRequestRef, onSignMessageRequestRef, From 45bd669d6a565fb8c202d37dcb40a720ea24e62c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 12:51:39 +0100 Subject: [PATCH 61/88] feat: allow to launch app in different contexts --- .../app/components/frame-app-debugger.tsx | 232 ++++++ .../app/components/frame-app-dialog.tsx | 148 ---- .../app/components/frame-debugger.tsx | 745 ++++++++++-------- packages/debugger/app/debugger-page.tsx | 13 + 4 files changed, 659 insertions(+), 479 deletions(-) create mode 100644 packages/debugger/app/components/frame-app-debugger.tsx delete mode 100644 packages/debugger/app/components/frame-app-dialog.tsx diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx new file mode 100644 index 000000000..223f12575 --- /dev/null +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -0,0 +1,232 @@ +import { Button } from "@/components/ui/button"; +import type { FrameLaunchedInContext } from "./frame-debugger"; +import { WithTooltip } from "./with-tooltip"; +import { Loader2Icon, RefreshCwIcon } from "lucide-react"; +import { Card, CardContent } from "@/components/ui/card"; +import { + FramePrimaryButton, + useFrameAppInIframe, +} from "@frames.js/render/use-frame-app"; +import { useCallback, useRef, useState } from "react"; +import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; +import { useWagmiProvider } from "@frames.js/render/frame-app/provider/wagmi"; +import { useToast } from "@/components/ui/use-toast"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { cn } from "@/lib/utils"; +import { DebuggerConsole } from "./debugger-console"; +import Image from "next/image"; +import { fallbackFrameContext } from "@frames.js/render"; +import { Console } from "console-feed"; +import type { Message } from "console-feed/lib/definitions/Component"; + +type TabValues = "events" | "console"; + +type FrameAppDebuggerProps = { + context: FrameLaunchedInContext; +}; + +export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { + const { toast } = useToast(); + const debuggerConsoleTabRef = useRef(null); + const iframeRef = useRef(null); + const [activeTab, setActiveTab] = useState("events"); + const [isAppReady, setIsAppReady] = useState(false); + const [events, setEvents] = useState([]); + const [primaryButton, setPrimaryButton] = useState<{ + button: FramePrimaryButton; + callback: () => void; + } | null>(null); + const farcasterSigner = useFarcasterIdentity(); + const provider = useWagmiProvider(); + const logEvent = useCallback((method: Message["method"], ...args: any[]) => { + setEvents((prev) => [ + ...prev, + { + id: prev.length.toString(), + method, + data: args, + }, + ]); + }, []); + const frameApp = useFrameAppInIframe({ + debug: true, + source: context.parseResult, + client: { + clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), + added: false, + }, + location: + context.context === "button_press" + ? { + type: "launcher", + } + : { + type: "cast_embed", + cast: fallbackFrameContext.castId, + }, + farcasterSigner, + provider, + proxyUrl: "/frames", + onReady(options) { + logEvent("info", "sdk.actions.ready() called", { options }); + setIsAppReady(true); + }, + onClose() { + logEvent("info", "sdk.actions.close() called"); + toast({ + title: "Frame app closed", + description: "The frame app called close() action.", + }); + }, + onOpenUrl(url) { + logEvent("info", "sdk.actions.openUrl() called", { url }); + window.open(url, "_blank"); + }, + onPrimaryButtonSet(button, buttonCallback) { + logEvent("info", "sdk.actions.setPrimaryButton() called", { button }); + setPrimaryButton({ + button, + callback: () => { + buttonCallback(); + }, + }); + }, + async onAddFrameRequested() { + logEvent("info", "sdk.actions.addFrame() called"); + + return false; + }, + onDebugEthProviderRequest(parameters) { + logEvent("info", "sdk.wallet.ethProvider.request() called", { + parameters, + }); + }, + }); + + return ( +
+
+
+ Reload frame app

}> + +
+
+
+
+
+ {frameApp.status === "pending" || + (!isAppReady && ( +
+
+ {`${name} +
+ +
+
+
+ ))} + {frameApp.status === "success" && ( + <> + - )} -
- {!!primaryButton && - primaryButton.button && - !primaryButton.button.hidden && ( - - - - )} - - - ); -} diff --git a/packages/debugger/app/components/frame-debugger.tsx b/packages/debugger/app/components/frame-debugger.tsx index dde084893..49573aa4d 100644 --- a/packages/debugger/app/components/frame-debugger.tsx +++ b/packages/debugger/app/components/frame-debugger.tsx @@ -34,7 +34,10 @@ import { FrameDebuggerRequestDetails } from "./frame-debugger-request-details"; import { FrameUI } from "./frame-ui"; import { useToast } from "@/components/ui/use-toast"; import { ToastAction } from "@/components/ui/toast"; -import { useDebuggerFrameState } from "../hooks/useDebuggerFrameState"; +import { + DebuggerFrameStackItem, + useDebuggerFrameState, +} from "../hooks/useDebuggerFrameState"; import { FrameDebuggerDiagnostics } from "./frame-debugger-diagnostics"; import { FrameDebuggerRequestCardContent } from "./frame-debugger-request-card-content"; import { useSharedFrameEventHandlers } from "../hooks/useSharedFrameEventHandlers"; @@ -43,12 +46,27 @@ import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; import { useXmtpIdentity } from "@frames.js/render/identity/xmtp"; import { useLensIdentity } from "@frames.js/render/identity/lens"; import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; -import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; +import type { + ParseFramesV2ResultWithFrameworkDetails, + ParseFramesWithReportsResult, +} from "frames.js/frame-parsers"; import { useFrameContext } from "../providers/FrameContextProvider"; -import type { LaunchFrameButtonPressEvent } from "@frames.js/render/unstable-types"; -import { FrameAppDialog } from "./frame-app-dialog"; import { cn } from "@/lib/utils"; import { FrameDebuggerFarcasterManifestDetails } from "./frame-debugger-farcaster-manifest-details"; +import { Frame, TriggerConfig } from "frames.js/farcaster-v2/types"; + +export type FrameLaunchedInContext = + | { + context: "trigger"; + triggerConfig: TriggerConfig; + frame: Frame; + parseResult: ParseFramesV2ResultWithFrameworkDetails; + } + | { + context: "button_press"; + frame: Frame; + parseResult: ParseFramesV2ResultWithFrameworkDetails; + }; type FrameDebuggerProps = { url: string; @@ -57,6 +75,7 @@ type FrameDebuggerProps = { hasExamples: boolean; protocol: ProtocolConfiguration; initialFrame?: ParseFramesWithReportsResult; + onFrameLaunchedInContext: (launchContext: FrameLaunchedInContext) => void; }; export type FrameDebuggerRef = { @@ -77,6 +96,7 @@ export const FrameDebugger = React.forwardRef< setMockHubContext, protocol, initialFrame, + onFrameLaunchedInContext, }, ref ) => { @@ -86,12 +106,6 @@ export const FrameDebugger = React.forwardRef< const lensSignerState = useLensIdentity(); const anonymousSignerState = useAnonymousIdentity(); const frameContext = useFrameContext(); - - const [launchedFrame, setLaunchedFrame] = useState | null>(null); - const sharedFrameEventHandlers = useSharedFrameEventHandlers({ debuggerRef: null, }); @@ -174,12 +188,13 @@ export const FrameDebugger = React.forwardRef< ), }); } else { - setLaunchedFrame(event); + onFrameLaunchedInContext({ + context: "button_press", + frame: event.frame, + parseResult: event.parseResult, + }); } }, - onLaunchedFrameClosed() { - setLaunchedFrame(null); - }, }); const debuggerConsoleTabRef = useRef(null); const [activeTab, setActiveTab] = useState("diagnostics"); @@ -246,339 +261,407 @@ export const FrameDebugger = React.forwardRef< !!currentFrameStackItem.frameResult.framesDebugInfo?.image; return ( - <> -
-
-
- Fetch home frame

}> - -
- Clear history and fetch home frame

}> - -
- Reload current frame

}> - -
-
- - - - - - {protocol.specification === "farcaster" && - mockHubContext && - setMockHubContext && ( - - )} - - - Debug - - -
- -
-
-
- +
+
+
+ Fetch home frame

}> + +
+ Clear history and fetch home frame

}> + +
+ Reload current frame

}> + +
-
-
- + + -
{url}
- - {!isLoading && protocol.specification !== "farcaster_v2" && ( - <> - {currentFrameStackItem?.request.method === "GET" && ( -
-

Preview

-
- -
-
- )} -
- {currentFrameStackItem?.status === "done" && - (currentFrameStackItem.frameResult.specification === - "farcaster" || - currentFrameStackItem.frameResult.specification === - "openframes") && - currentFrameStackItem.frameResult.frame.buttons - ?.filter( - (button) => - button.target?.startsWith( - "https://warpcast.com/~/add-cast-action" - ) || - button.target?.startsWith( - "https://warpcast.com/~/composer-action" - ) - ) - .map((button) => { - // Link to debug target - return ( -
+
+
+ +
{url}
- router.push(`/?${params.toString()}`); - }} - style={{ - flex: "1 1 0px", - // fixme: hover style - backgroundColor: defaultTheme.buttonBg, - borderColor: defaultTheme.buttonBorderColor, - color: defaultTheme.buttonColor, - cursor: "pointer", - }} - > - - - Debug{" "} - - {button.label} - - - - ); - })} -
- + {!isLoading && + currentFrameStackItem && + protocol.specification === "farcaster_v2" && ( + )} -
-
-
- {currentFrameStackItem ? ( - - - setActiveTab(value as TabValues)} - className="grid grid-rows-[auto_1fr] w-full h-full" - > - - Diagnostics - Console - Request - {protocol.protocol === "farcaster_v2" && ( - Manifest - )} - Meta Tags - - - - - - { - if ( - wantsToScrollConsoleToBottomRef.current && - debuggerConsoleTabRef.current - ) { - wantsToScrollConsoleToBottomRef.current = false; - debuggerConsoleTabRef.current.scrollTo( - 0, - element.scrollHeight - ); - } - }} - /> - - - + {currentFrameStackItem?.request.method === "GET" && ( +
+

Preview

+
+ - - {protocol.protocol === "farcaster_v2" && ( - - - - )} - - {currentFrameStackItem.status === "done" ? ( -
- html tags +
+
+ )} +
+ {currentFrameStackItem?.status === "done" && + (currentFrameStackItem.frameResult.specification === + "farcaster" || + currentFrameStackItem.frameResult.specification === + "openframes") && + currentFrameStackItem.frameResult.frame.buttons + ?.filter( + (button) => + button.target?.startsWith( + "https://warpcast.com/~/add-cast-action" + ) || + button.target?.startsWith( + "https://warpcast.com/~/composer-action" + ) + ) + .map((button) => { + // Link to debug target + return ( -
-                            {currentFrameStackItem.frameResult.specification ===
-                            "farcaster_v2"
-                              ? getFrameV2HtmlHead(
-                                  currentFrameStackItem.frameResult.frame
-                                )
-                              : getFrameHtmlHead(
-                                  "sourceFrame" in
-                                    currentFrameStackItem.request &&
-                                    currentFrameStackItem.request.sourceFrame
-                                    ? currentFrameStackItem.request.sourceFrame
-                                    : currentFrameStackItem.frameResult.frame
-                                )
-                                  .split(" !!t)
-                                  // hacky...
-                                  .flatMap((el, i) => [
-                                    {`,
-                                    
, - ])} -
-
- ) : null} - - - - - ) : null} + + + Debug{" "} + {button.label} + + + ); + })} +
+ + )}
- {launchedFrame && ( - frameState.onLaunchedFrameClose()} - /> - )} - +
+ {currentFrameStackItem ? ( + + + setActiveTab(value as TabValues)} + className="grid grid-rows-[auto_1fr] w-full h-full" + > + + Diagnostics + Console + Request + {protocol.protocol === "farcaster_v2" && ( + Manifest + )} + Meta Tags + + + + + + { + if ( + wantsToScrollConsoleToBottomRef.current && + debuggerConsoleTabRef.current + ) { + wantsToScrollConsoleToBottomRef.current = false; + debuggerConsoleTabRef.current.scrollTo( + 0, + element.scrollHeight + ); + } + }} + /> + + + + + {protocol.protocol === "farcaster_v2" && ( + + + + )} + + {currentFrameStackItem.status === "done" ? ( +
+ html tags + +
+                          {currentFrameStackItem.frameResult.specification ===
+                          "farcaster_v2"
+                            ? getFrameV2HtmlHead(
+                                currentFrameStackItem.frameResult.frame
+                              )
+                            : getFrameHtmlHead(
+                                "sourceFrame" in
+                                  currentFrameStackItem.request &&
+                                  currentFrameStackItem.request.sourceFrame
+                                  ? currentFrameStackItem.request.sourceFrame
+                                  : currentFrameStackItem.frameResult.frame
+                              )
+                                .split(" !!t)
+                                // hacky...
+                                .flatMap((el, i) => [
+                                  {`,
+                                  
, + ])} +
+
+ ) : null} +
+
+
+
+ ) : null} +
+
); } ); FrameDebugger.displayName = "FrameDebugger"; + +type FrameV2TriggerButtonsProps = { + stackItem: DebuggerFrameStackItem; + onLaunchFrameButtonPressed: ( + event: Extract + ) => void; +}; + +function FrameV2TriggerButtons({ + stackItem, + onLaunchFrameButtonPressed, +}: FrameV2TriggerButtonsProps) { + if (stackItem.status !== "done") { + return null; + } + + if ( + stackItem.frameResult.specification !== "farcaster_v2" || + stackItem.frameResult.status !== "success" + ) { + return null; + } + + const parseResult = stackItem.frameResult; + const frame = parseResult.frame; + const manifestParseResult = parseResult.manifest; + + if (!manifestParseResult || manifestParseResult.status !== "success") { + return null; + } + + const manifest = manifestParseResult.manifest; + + if (!manifest.triggers || manifest.triggers.length === 0) { + return null; + } + + return ( +
+

Triggers

+ {manifest.triggers.map((triggerConfig, i) => { + return ( + + ); + })} +
+ ); +} diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index 3b7db3b2f..bcc35d31f 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -18,6 +18,7 @@ import { useAccount } from "wagmi"; import pkg from "../package.json"; import { FrameDebugger, + FrameLaunchedInContext, type FrameDebuggerRef, } from "./components/frame-debugger"; import { LOCAL_STORAGE_KEYS } from "./constants"; @@ -57,6 +58,7 @@ import { import { useFarcasterIdentity } from "./hooks/useFarcasterIdentity"; import { ProtocolSelectorProvider } from "./providers/ProtocolSelectorProvider"; import { FrameContextProvider } from "./providers/FrameContextProvider"; +import { FrameAppDebugger } from "./components/frame-app-debugger"; const FALLBACK_URL = process.env.NEXT_PUBLIC_DEBUGGER_DEFAULT_URL || "http://localhost:3000"; @@ -78,6 +80,8 @@ export default function DebuggerPage({ const router = useRouter(); const [protocolConfiguration, setProtocolConfiguration] = useState(null); + const [frameV2LaunchContext, setFrameV2LaunchContext] = + useState(null); /** * Parse the URL from the query string. This will also cause debugger to automatically load the frame. */ @@ -168,9 +172,11 @@ export default function DebuggerPage({ if (json.type === "action") { setInitialAction(json); setInitialFrame(undefined); + setFrameV2LaunchContext(null); } else if (json.type === "frame") { setInitialFrame(json); setInitialAction(undefined); + setFrameV2LaunchContext(null); } }) .catch((e) => { @@ -447,8 +453,15 @@ export default function DebuggerPage({ protocol={protocolConfiguration} ref={debuggerRef} hasExamples={!!examples} + onFrameLaunchedInContext={setFrameV2LaunchContext} /> )} + + {initialFrame && + !!protocolConfiguration && + frameV2LaunchContext && ( + + )} ) : ( examples From dab237b241494ab2904d2c4bd67a482994657eba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 16:09:15 +0100 Subject: [PATCH 62/88] fix: respect abort signal --- .../src/frame-app/use-fetch-frame-app.ts | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/packages/render/src/frame-app/use-fetch-frame-app.ts b/packages/render/src/frame-app/use-fetch-frame-app.ts index fe1fa5405..0a1b7804c 100644 --- a/packages/render/src/frame-app/use-fetch-frame-app.ts +++ b/packages/render/src/frame-app/use-fetch-frame-app.ts @@ -60,6 +60,10 @@ export function useFetchFrameApp({ // we don't want to return promise from fetchFrame because it is used in effect Promise.resolve(sourceUrl) .then(async (url) => { + setState({ + status: "pending", + }); + const responseOrError = await fetchProxied({ fetchFn: fetchRef.current, url: url.toString(), @@ -86,6 +90,10 @@ export function useFetchFrameApp({ ); } + if (abortController.signal.aborted) { + return; + } + setState({ status: "success", frame: data.farcaster_v2, @@ -93,23 +101,16 @@ export function useFetchFrameApp({ }); }) .catch((e) => { + if (abortController.signal.aborted) { + return; + } + setState({ status: "error", error: e instanceof Error ? e : new Error(String(e)), }); }); - try { - setState({ - status: "pending", - }); - } catch (e) { - setState({ - status: "error", - error: e instanceof Error ? e : new Error(String(e)), - }); - } - return () => { abortController.abort(); }; From 13faad27d5a7378a72598fde87948e2bc3818749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 16:09:27 +0100 Subject: [PATCH 63/88] feat: allow to resolve client --- .../src/frame-app/use-resolve-client.ts | 101 ++++++++++++++++++ packages/render/src/use-frame-app.ts | 34 +++++- 2 files changed, 131 insertions(+), 4 deletions(-) create mode 100644 packages/render/src/frame-app/use-resolve-client.ts diff --git a/packages/render/src/frame-app/use-resolve-client.ts b/packages/render/src/frame-app/use-resolve-client.ts new file mode 100644 index 000000000..320685d4a --- /dev/null +++ b/packages/render/src/frame-app/use-resolve-client.ts @@ -0,0 +1,101 @@ +import { useCallback, useEffect, useRef, useState } from "react"; +import type { FrameClientConfig } from "./types"; + +export type ResolveClientFunction = (options: { + signal: AbortSignal; +}) => Promise; + +type UseResolveClientOptions = { + client: FrameClientConfig | ResolveClientFunction; +}; + +type UseResolveClientResult = + | { + status: "success"; + client: FrameClientConfig; + } + | { + status: "error"; + error: Error; + } + | { + status: "pending"; + }; + +export function useResolveClient({ + client, +}: UseResolveClientOptions): UseResolveClientResult { + const abortControllerRef = useRef(null); + const [state, setState] = useState(() => { + if (typeof client !== "function") { + return { + status: "success", + client, + }; + } + + return { + status: "pending", + }; + }); + + const resolveClient = useCallback((resolve: ResolveClientFunction) => { + // cancel previous request + abortControllerRef.current?.abort(); + + const abortController = new AbortController(); + abortControllerRef.current = abortController; + + Promise.resolve() + .then(async () => { + setState({ + status: "pending", + }); + const resolvedClient = await resolve({ + signal: abortController.signal, + }); + + if (abortController.signal.aborted) { + return; + } + + setState({ + status: "success", + client: resolvedClient, + }); + }) + .catch((e) => { + if (abortController.signal.aborted) { + return; + } + + setState({ + status: "error", + error: e instanceof Error ? e : new Error(String(e)), + }); + }); + + return () => { + abortController.abort(); + }; + }, []); + + useEffect(() => { + if (typeof client !== "function") { + setState((prevState) => { + if (prevState.status === "success" && prevState.client !== client) { + return { + status: "success", + client, + }; + } + + return prevState; + }); + } else { + resolveClient(client); + } + }, [client, resolveClient]); + + return state; +} diff --git a/packages/render/src/use-frame-app.ts b/packages/render/src/use-frame-app.ts index fc48238c6..bc52c0108 100644 --- a/packages/render/src/use-frame-app.ts +++ b/packages/render/src/use-frame-app.ts @@ -24,6 +24,12 @@ import type { EthProvider } from "./frame-app/provider/types"; import type { FrameClientConfig, FrameEvent } from "./frame-app/types"; import { assertNever } from "./assert-never"; import { useFetchFrameApp } from "./frame-app/use-fetch-frame-app"; +import { + type ResolveClientFunction, + useResolveClient, +} from "./frame-app/use-resolve-client"; + +export type { ResolveClientFunction, FrameClientConfig }; export type SendTransactionRpcRequest = ExtractRequest< DefaultRpcSchema, @@ -167,8 +173,13 @@ export type UseFrameAppOptions = { provider: EthProvider; /** * Frame client that is rendering the app + * + * This is async function if you need to fetch the client configuration + * like notification settings, etc. + * + * Value should be memoized otherwise it will cause unnecessary re-renders. */ - client: FrameClientConfig; + client: FrameClientConfig | ResolveClientFunction; /** * Information about the context from which the frame was launched. * @@ -280,6 +291,7 @@ export type UseFrameAppReturn = */ registerEndpoint: RegisterEndpointFunction; frame: ParseFramesV2ResultWithFrameworkDetails; + client: FrameClientConfig; status: "success"; } | { @@ -320,7 +332,6 @@ export function useFrameApp({ }: UseFrameAppOptions): UseFrameAppReturn { const providerRef = useFreshRef(provider); const debugRef = useFreshRef(debug); - const clientRef = useFreshRef(client); const locationRef = useFreshRef(location); const readyRef = useFreshRef(onReady); const closeRef = useFreshRef(onClose); @@ -334,6 +345,7 @@ export function useFrameApp({ const onSignTypedDataRequestRef = useFreshRef(onSignTypedDataRequest); const addFrameRequestsCacheRef = useFreshRef(addFrameRequestsCache); const onDebugEthProviderRequestRef = useFreshRef(onDebugEthProviderRequest); + const clientResolutionState = useResolveClient({ client }); const frameResolutionState = useFetchFrameApp({ source, fetchFn, @@ -359,6 +371,19 @@ export function useFrameApp({ providerRef.current.emitter.setDebugMode(debug); return useMemo(() => { + if (clientResolutionState.status === "pending") { + return { + status: "pending", + }; + } + + if (clientResolutionState.status === "error") { + return { + status: "error", + error: clientResolutionState.error, + }; + } + switch (frameResolutionState.status) { case "success": { const frame = frameResolutionState.frame; @@ -409,7 +434,7 @@ export function useFrameApp({ ); return { user: { fid: signer.fid }, - client: clientRef.current, + client: clientResolutionState.client, location: locationRef.current, }; }, @@ -551,6 +576,7 @@ export function useFrameApp({ }, status: "success", frame: frameResolutionState.frame, + client: clientResolutionState.client, }; } case "error": { @@ -568,13 +594,13 @@ export function useFrameApp({ assertNever(frameResolutionState); } }, [ + clientResolutionState, frameResolutionState, logDebugRef, farcasterSignerRef, providerRef, onSignerNotApprovedRef, closeRef, - clientRef, locationRef, onOpenUrlRef, readyRef, From f8593fbb31458c1004c32d650797f47f8b26b848 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Fri, 13 Dec 2024 16:10:05 +0100 Subject: [PATCH 64/88] feat: simple notifications support --- packages/debugger/.env.sample | 8 +- .../frame-app-debugger-notifications.tsx | 141 +++++++++++++++ .../app/components/frame-app-debugger.tsx | 161 ++++++++++++++++-- packages/debugger/app/constants.ts | 2 + packages/debugger/app/debugger-page.tsx | 5 +- packages/debugger/app/lib/redis.ts | 12 ++ .../debugger/app/notifications/[id]/route.ts | 62 +++++++ packages/debugger/app/notifications/route.ts | 105 ++++++++++++ packages/debugger/globals.d.ts | 4 + packages/debugger/package.json | 2 + yarn.lock | 7 + 11 files changed, 494 insertions(+), 15 deletions(-) create mode 100644 packages/debugger/app/components/frame-app-debugger-notifications.tsx create mode 100644 packages/debugger/app/lib/redis.ts create mode 100644 packages/debugger/app/notifications/[id]/route.ts create mode 100644 packages/debugger/app/notifications/route.ts diff --git a/packages/debugger/.env.sample b/packages/debugger/.env.sample index 430fe1cb7..95c873a3a 100644 --- a/packages/debugger/.env.sample +++ b/packages/debugger/.env.sample @@ -8,4 +8,10 @@ FARCASTER_DEVELOPER_MNEMONIC= # Example: FARCASTER_DEVELOPER_FID=1214 FARCASTER_DEVELOPER_FID= -NEXT_PUBLIC_WALLETCONNECT_ID= \ No newline at end of file +NEXT_PUBLIC_WALLETCONNECT_ID= + +# Required to debug Farcaster Frames v2 notifications +KV_URL="" +KV_REST_API_READ_ONLY_TOKEN="" +KV_REST_API_TOKEN="" +KV_REST_API_URL="" \ No newline at end of file diff --git a/packages/debugger/app/components/frame-app-debugger-notifications.tsx b/packages/debugger/app/components/frame-app-debugger-notifications.tsx new file mode 100644 index 000000000..5ce99e93d --- /dev/null +++ b/packages/debugger/app/components/frame-app-debugger-notifications.tsx @@ -0,0 +1,141 @@ +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Console } from "console-feed"; +import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; +import type { NotificationSettings } from "../notifications/route"; +import { useCallback, useEffect, useState } from "react"; +import { Message } from "console-feed/lib/definitions/Component"; +import type { Notification } from "../notifications/[id]/route"; + +type FrameAppDebuggerNotificationsProps = { + frame: ParseFramesV2ResultWithFrameworkDetails; + notificationSettings: NotificationSettings["details"] | null | undefined; +}; + +export function FrameAppDebuggerNotifications({ + frame, + notificationSettings, +}: FrameAppDebuggerNotificationsProps) { + const [notifications, setNotifications] = useState([]); + const loadNotifications = useCallback(async (url: string) => { + try { + const response = await fetch(url, { + method: "GET", + }); + + if (!response.ok) { + return; + } + + const notifications = (await response.json()) as Notification[]; + + if (notifications.length === 0) { + return; + } + + setNotifications((prevNotifications) => { + return [ + ...prevNotifications, + ...notifications.map((notification): Message => { + return { + method: "log", + id: notification.notificationId, + data: ["Received notification", notification], + }; + }), + ]; + }); + } catch (e) {} + }, []); + + useEffect(() => { + // if url is set then start fetching notifications + if (!notificationSettings?.url) { + return; + } + + // maybe use server sent events instead + const interval = window.setInterval( + () => loadNotifications(notificationSettings.url), + 5000 + ); + + return () => { + window.clearInterval(interval); + }; + }, [loadNotifications, notificationSettings?.url]); + + if (frame.status !== "success") { + return ( + <> + + Invalid frame! + + Please check the diagnostics of initial frame + + + + ); + } + + if (!frame.manifest) { + return ( + <> + + Missing manifest + + Please check the diagnostics of initial frame + + + + ); + } + + if (frame.manifest.status === "failure") { + return ( + <> + + Invalid manifest! + + Please check the diagnostics of initial frame + + + + ); + } + + if (!frame.manifest.manifest.frame.webhookUrl) { + return ( + <> + + Missing webhookUrl + + Frame manifest must contain webhookUrl property in order to support + notifications. + + + + ); + } + + // @todo we should show a way to enable notifications + if (!notificationSettings) { + return ( + <> + + Notifications are not enabled + + In order debug the notifications you should allow the app to send + notifications. + + + + ); + } + + // @todo add a way to send to webhook different events + return ( +
+ +
+ ); +} diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 223f12575..5ce97a4c5 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -4,11 +4,12 @@ import { WithTooltip } from "./with-tooltip"; import { Loader2Icon, RefreshCwIcon } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; import { - FramePrimaryButton, + type FramePrimaryButton, + type ResolveClientFunction, + type FrameClientConfig, useFrameAppInIframe, } from "@frames.js/render/use-frame-app"; import { useCallback, useRef, useState } from "react"; -import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; import { useWagmiProvider } from "@frames.js/render/frame-app/provider/wagmi"; import { useToast } from "@/components/ui/use-toast"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; @@ -18,14 +19,25 @@ import Image from "next/image"; import { fallbackFrameContext } from "@frames.js/render"; import { Console } from "console-feed"; import type { Message } from "console-feed/lib/definitions/Component"; +import type { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; +import type { NotificationSettings } from "../notifications/route"; +import { FrameAppDebuggerNotifications } from "./frame-app-debugger-notifications"; -type TabValues = "events" | "console"; +type TabValues = "events" | "console" | "notifications"; type FrameAppDebuggerProps = { context: FrameLaunchedInContext; + farcasterSigner: FarcasterSignerInstance; }; -export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { +const addFrameRequestsCache = new Set(); + +export function FrameAppDebugger({ + context, + farcasterSigner, +}: FrameAppDebuggerProps) { + const farcasterSignerRef = useRef(farcasterSigner); + farcasterSignerRef.current = farcasterSigner; const { toast } = useToast(); const debuggerConsoleTabRef = useRef(null); const iframeRef = useRef(null); @@ -36,7 +48,6 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { button: FramePrimaryButton; callback: () => void; } | null>(null); - const farcasterSigner = useFarcasterIdentity(); const provider = useWagmiProvider(); const logEvent = useCallback((method: Message["method"], ...args: any[]) => { setEvents((prev) => [ @@ -48,13 +59,57 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { }, ]); }, []); + const resolveClient: ResolveClientFunction = useCallback(async () => { + const signer = farcasterSignerRef.current.signer; + let notificationDetails: FrameClientConfig["notificationDetails"]; + + if (signer?.status !== "approved") { + console.debug( + "Signer not approved, cannot resolve notification settings" + ); + } else { + const response = await fetch("/notifications", { + headers: { + "x-fid": signer.fid.toString(), + "x-frame-app-url": context.frame.button.action.url, + }, + }); + + if (response.status === 200) { + const data = await response.json(); + + notificationDetails = data; + } else if (response.status === 403) { + toast({ + title: "Access Forbidden", + description: "Debugger is probably misconfigured.", + variant: "destructive", + }); + } else if (response.status === 401) { + toast({ + title: "Not Authenticated", + description: "Check that you have approved farcaster signer.", + variant: "destructive", + }); + } + } + + return { + clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), + added: !!notificationDetails, + notificationDetails, + }; + }, [context.frame.button.action.url, toast]); + /** + * Undefined means that it was never changed, so we should use the value from client + */ + const [notificationSettings, setNotificationSettings] = useState< + NotificationSettings["details"] | null | undefined + >(undefined); const frameApp = useFrameAppInIframe({ debug: true, source: context.parseResult, - client: { - clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), - added: false, - }, + client: resolveClient, location: context.context === "button_press" ? { @@ -67,6 +122,7 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { farcasterSigner, provider, proxyUrl: "/frames", + addFrameRequestsCache, onReady(options) { logEvent("info", "sdk.actions.ready() called", { options }); setIsAppReady(true); @@ -91,10 +147,78 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { }, }); }, - async onAddFrameRequested() { + async onAddFrameRequested(parseResult) { + // this is not compliant with how it should work in production + // but this is debugger, what if you want to test your function repeately + addFrameRequestsCache.clear(); + logEvent("info", "sdk.actions.addFrame() called"); - return false; + if (farcasterSigner.signer?.status !== "approved") { + toast({ + title: "Signer not approved", + description: + "Signer is not approved. Approved farcaster signer is necessary.", + variant: "destructive", + }); + + return false; + } + + const webhookUrl = parseResult.manifest?.manifest.frame?.webhookUrl; + + if (!webhookUrl) { + toast({ + title: "Webhook URL not found", + description: + "Webhook URL is not found in the manifest. It is required in order to enable notifications.", + variant: "destructive", + }); + + return false; + } + + // check what is the status of notifications for this app and signer + // if there are no settings ask for user's consent and store the result + const consent = window.confirm( + "Do you want to add the frame to the app?" + ); + + if (!consent) { + return false; + } + + // register the settings + const response = await fetch("/notifications", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + fid: farcasterSigner.signer.fid, + frameUrl: context.frame.button.action.url, + webhookUrl, + }), + }); + + if (response.status !== 201) { + toast({ + title: "Failed to register notifications", + description: "Failed to register notifications for this app.", + variant: "destructive", + }); + + return false; + } + + const settings = (await response.json()) as NotificationSettings; + + setNotificationSettings(settings.details); + + return { + added: true, + notificationDetails: settings.details, + }; }, onDebugEthProviderRequest(parameters) { logEvent("info", "sdk.wallet.ethProvider.request() called", { @@ -191,7 +315,7 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) {
- {frameApp.status !== "pending" ? ( + {frameApp.status === "success" ? ( setActiveTab(value as TabValues)} className="grid grid-rows-[auto_1fr] w-full h-full" > - + Events Console + Notifications @@ -222,6 +347,16 @@ export function FrameAppDebugger({ context }: FrameAppDebuggerProps) { }} /> + + + diff --git a/packages/debugger/app/constants.ts b/packages/debugger/app/constants.ts index fddb6cc56..6884fa0a0 100644 --- a/packages/debugger/app/constants.ts +++ b/packages/debugger/app/constants.ts @@ -1,3 +1,5 @@ export const LOCAL_STORAGE_KEYS = { SELECTED_PROTOCOL: "selectedProtocol", }; + +export const NOTIFICATION_TTL_IN_SECONDS = 10; diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index bcc35d31f..b6956bdd0 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -460,7 +460,10 @@ export default function DebuggerPage({ {initialFrame && !!protocolConfiguration && frameV2LaunchContext && ( - + )} ) : ( diff --git a/packages/debugger/app/lib/redis.ts b/packages/debugger/app/lib/redis.ts new file mode 100644 index 000000000..69fea85af --- /dev/null +++ b/packages/debugger/app/lib/redis.ts @@ -0,0 +1,12 @@ +import { Redis } from "@upstash/redis"; + +export function createRedis() { + if (!process.env.KV_REST_API_TOKEN || !process.env.KV_REST_API_URL) { + throw new Error("Missing KV_REST_API_TOKEN or KV_REST_API_URL"); + } + + return new Redis({ + url: process.env.KV_REST_API_URL, + token: process.env.KV_REST_API_TOKEN, + }); +} diff --git a/packages/debugger/app/notifications/[id]/route.ts b/packages/debugger/app/notifications/[id]/route.ts new file mode 100644 index 000000000..d0856b332 --- /dev/null +++ b/packages/debugger/app/notifications/[id]/route.ts @@ -0,0 +1,62 @@ +import { + sendNotificationRequestSchema, + type SendNotificationRequest, + type SendNotificationResponse, +} from "@farcaster/frame-sdk"; +import { NextRequest } from "next/server"; +import { createRedis } from "../../lib/redis"; +import { NOTIFICATION_TTL_IN_SECONDS } from "../../constants"; + +export type NotificationUrl = { + token: string; +}; + +export type Notification = Omit; + +export async function POST(req: NextRequest): Promise { + const redis = createRedis(); + const notificationUrl = req.nextUrl.href; + const parseResult = sendNotificationRequestSchema.safeParse(await req.json()); + + if (!parseResult.success) { + return Response.json(parseResult.error.flatten(), { status: 400 }); + } + + const settings = await redis.get(notificationUrl); + + if (!settings) { + return Response.json({ message: "Not Found" }, { status: 404 }); + } + + const { tokens, ...notification } = parseResult.data; + + await redis.lpush(notificationUrl + ":list", notification); + await redis.expire(notificationUrl, NOTIFICATION_TTL_IN_SECONDS); + + return Response.json( + { + result: { + successfulTokens: tokens, + invalidTokens: [], + rateLimitedTokens: [], + }, + } satisfies SendNotificationResponse, + { status: 200 } + ); +} + +export async function GET(req: NextRequest): Promise { + const redis = createRedis(); + const notificationUrl = req.nextUrl.href; + + const notifications = await redis.lpop( + notificationUrl + ":list", + 10 + ); + + return Response.json(notifications || [], { + headers: { + "Cache-Control": "no-store, no-cache", + }, + }); +} diff --git a/packages/debugger/app/notifications/route.ts b/packages/debugger/app/notifications/route.ts new file mode 100644 index 000000000..b7d7a0c1c --- /dev/null +++ b/packages/debugger/app/notifications/route.ts @@ -0,0 +1,105 @@ +import { FrameClientConfig } from "@frames.js/render/use-frame-app"; +import type { NextRequest } from "next/server"; +import { z } from "zod"; +import crypto from "node:crypto"; +import { createRedis } from "../lib/redis"; +import type { NotificationUrl } from "./[id]/route"; +import { NOTIFICATION_TTL_IN_SECONDS } from "../constants"; + +function validateAuth(req: Request) { + const fid = req.headers.get("x-fid"); + const frameAppUrl = req.headers.get("x-frame-app-url"); + + if (!fid || !frameAppUrl) { + return false; + } + + return { + fid, + frameAppUrl, + }; +} + +function getFrameAppNotificationSettingsKey(fid: string, frameAppUrl: string) { + return `frame-app-notification:${fid}:${frameAppUrl}`; +} + +export type NotificationSettings = { + details: NonNullable; + webhookUrl: string; +}; + +/** + * Checks for the given notifications settings of user and url + */ +export async function GET(req: NextRequest): Promise { + const auth = validateAuth(req); + + if (!auth) { + return Response.json({ message: "Not Authenticated" }, { status: 401 }); + } + + const redis = createRedis(); + + const { fid, frameAppUrl } = auth; + const key = getFrameAppNotificationSettingsKey(fid, frameAppUrl); + + const settings = await redis.get(key); + + if (!settings) { + return Response.json({ message: "Not Found" }, { status: 404 }); + } + + return Response.json(settings, { + headers: { + "Cache-Control": "no-store, no-cache", + }, + }); +} + +const bodySchema = z.object({ + fid: z.coerce.number().int().min(1), + frameUrl: z.string().url(), + webhookUrl: z.string().url(), +}); + +/** + * Registers notification for the given url and user + */ +export async function POST(req: NextRequest): Promise { + const { fid, frameUrl, webhookUrl } = bodySchema.parse(await req.json()); + const redis = createRedis(); + const key = getFrameAppNotificationSettingsKey(fid.toString(), frameUrl); + const token = crypto.randomUUID(); + const notificationUrl = new URL(`/notifications/${token}`, req.nextUrl.href); + + const settings: NotificationSettings = { + webhookUrl, + details: { + token: crypto.randomUUID(), + url: notificationUrl.toString(), + }, + }; + + await redis.set( + notificationUrl.toString(), + { token }, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } + ); + const result = await redis.set(key, settings, { + ex: NOTIFICATION_TTL_IN_SECONDS, + }); + + if (!result) { + return Response.json( + { message: "Failed to save settings" }, + { status: 500 } + ); + } + + // @todo call webhook? + + return Response.json(settings, { status: 201 }); +} diff --git a/packages/debugger/globals.d.ts b/packages/debugger/globals.d.ts index 188f15fe6..91fa0c951 100644 --- a/packages/debugger/globals.d.ts +++ b/packages/debugger/globals.d.ts @@ -19,6 +19,10 @@ declare global { * FID to be attributed for farcaster frame transactions. */ NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID: string | undefined; + + KV_URL: string | undefined; + KV_REST_API_TOKEN: string | undefined; + KV_REST_API_URL: string | undefined; } } } diff --git a/packages/debugger/package.json b/packages/debugger/package.json index 890803574..bdece7669 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -16,7 +16,9 @@ "url": "https://github.com/framesjs/frames.js/tree/main/packages/debugger" }, "dependencies": { + "@upstash/redis": "^1.34.3", "@lens-protocol/client": "2.3.2", + "@farcaster/frame-sdk": "^0.0.16", "@xmtp/xmtp-js": "^12.0.0", "is-port-reachable": "^4.0.0", "next": "14.1.4", diff --git a/yarn.lock b/yarn.lock index c91dc6c99..04f661442 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6046,6 +6046,13 @@ dependencies: crypto-js "^4.2.0" +"@upstash/redis@^1.34.3": + version "1.34.3" + resolved "https://registry.yarnpkg.com/@upstash/redis/-/redis-1.34.3.tgz#df0338f4983bba5141878e851be4fced494b44a0" + integrity sha512-VT25TyODGy/8ljl7GADnJoMmtmJ1F8d84UXfGonRRF8fWYJz7+2J6GzW+a6ETGtk4OyuRTt7FRSvFG5GvrfSdQ== + dependencies: + crypto-js "^4.2.0" + "@vanilla-extract/babel-plugin-debug-ids@^1.0.4": version "1.0.5" resolved "https://registry.yarnpkg.com/@vanilla-extract/babel-plugin-debug-ids/-/babel-plugin-debug-ids-1.0.5.tgz#e24424f46dd7737764a4bb5ac6dcdf19240f88bc" From d3b0016bb4239b6e7736bfa15342586755b37988 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Sat, 14 Dec 2024 14:37:10 +0100 Subject: [PATCH 65/88] chore: do not allow to run frame app debugger from cast action debugger --- packages/debugger/app/components/cast-action-debugger.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/packages/debugger/app/components/cast-action-debugger.tsx b/packages/debugger/app/components/cast-action-debugger.tsx index f3c7000ed..9fcea10fb 100644 --- a/packages/debugger/app/components/cast-action-debugger.tsx +++ b/packages/debugger/app/components/cast-action-debugger.tsx @@ -171,6 +171,13 @@ export function CastActionDebugger({ }} mockHubContext={mockHubContext} setMockHubContext={setMockHubContext} + onFrameLaunchedInContext={() => { + toast({ + title: "Frame v2 is not supported in cast action debugger.", + description: "Please use the frame debugger instead.", + variant: "destructive", + }); + }} /> )} From 5b8085e94572b122d12d335e69e0b66333755d86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Tue, 17 Dec 2024 09:50:43 +0100 Subject: [PATCH 66/88] feat: allow to manage frame and notification settings --- .../frame-app-debugger-notifications.tsx | 131 +++++---- .../app/components/frame-app-debugger.tsx | 235 ++++++++------- .../frame-app-notifications-control-panel.tsx | 176 +++++++++++ .../debugger/app/notifications/[id]/route.ts | 77 +++-- packages/debugger/app/notifications/auth.ts | 13 + .../debugger/app/notifications/helpers.ts | 8 + packages/debugger/app/notifications/route.ts | 133 +++++---- .../debugger/app/notifications/storage.ts | 232 +++++++++++++++ packages/debugger/app/notifications/types.ts | 38 +++ .../FrameAppNotificationsManagerProvider.tsx | 278 ++++++++++++++++++ packages/debugger/package.json | 4 +- yarn.lock | 20 +- 12 files changed, 1077 insertions(+), 268 deletions(-) create mode 100644 packages/debugger/app/components/frame-app-notifications-control-panel.tsx create mode 100644 packages/debugger/app/notifications/auth.ts create mode 100644 packages/debugger/app/notifications/helpers.ts create mode 100644 packages/debugger/app/notifications/storage.ts create mode 100644 packages/debugger/app/notifications/types.ts create mode 100644 packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx diff --git a/packages/debugger/app/components/frame-app-debugger-notifications.tsx b/packages/debugger/app/components/frame-app-debugger-notifications.tsx index 5ce99e93d..5e6fc67d6 100644 --- a/packages/debugger/app/components/frame-app-debugger-notifications.tsx +++ b/packages/debugger/app/components/frame-app-debugger-notifications.tsx @@ -1,68 +1,66 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Console } from "console-feed"; +import { InboxIcon, Loader2Icon } from "lucide-react"; import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; -import type { NotificationSettings } from "../notifications/route"; -import { useCallback, useEffect, useState } from "react"; +import { useEffect, useState } from "react"; import { Message } from "console-feed/lib/definitions/Component"; import type { Notification } from "../notifications/[id]/route"; +import { useQuery } from "@tanstack/react-query"; +import { FrameAppNotificationsControlPanel } from "./frame-app-notifications-control-panel"; +import { useFrameAppNotificationsManagerContext } from "../providers/FrameAppNotificationsManagerProvider"; type FrameAppDebuggerNotificationsProps = { frame: ParseFramesV2ResultWithFrameworkDetails; - notificationSettings: NotificationSettings["details"] | null | undefined; }; export function FrameAppDebuggerNotifications({ frame, - notificationSettings, }: FrameAppDebuggerNotificationsProps) { + const frameAppNotificationManager = useFrameAppNotificationsManagerContext(); const [notifications, setNotifications] = useState([]); - const loadNotifications = useCallback(async (url: string) => { - try { - const response = await fetch(url, { - method: "GET", - }); - - if (!response.ok) { - return; + const notificationsQuery = useQuery({ + initialData: [], + enabled: frameAppNotificationManager.state?.enabled ?? false, + queryKey: [ + "frame-app-notifications-log", + frameAppNotificationManager.state?.enabled + ? frameAppNotificationManager.state.details.url + : null, + ], + async queryFn() { + if (!frameAppNotificationManager.state?.enabled) { + return [] as Message[]; } - const notifications = (await response.json()) as Notification[]; + const response = await fetch( + frameAppNotificationManager.state.details.url, + { + method: "GET", + } + ); - if (notifications.length === 0) { - return; + if (!response.ok) { + return [] as Message[]; } - setNotifications((prevNotifications) => { - return [ - ...prevNotifications, - ...notifications.map((notification): Message => { - return { - method: "log", - id: notification.notificationId, - data: ["Received notification", notification], - }; - }), - ]; + const data = (await response.json()) as Notification[]; + + return data.map((notification): Message => { + return { + method: "log", + id: notification.notificationId, + data: ["Received notification", notification], + }; }); - } catch (e) {} - }, []); + }, + refetchInterval: 5000, + }); useEffect(() => { - // if url is set then start fetching notifications - if (!notificationSettings?.url) { - return; + if (notificationsQuery.data) { + setNotifications((prev) => [...prev, ...notificationsQuery.data]); } - - // maybe use server sent events instead - const interval = window.setInterval( - () => loadNotifications(notificationSettings.url), - 5000 - ); - - return () => { - window.clearInterval(interval); - }; - }, [loadNotifications, notificationSettings?.url]); + }, [notificationsQuery.data]); if (frame.status !== "success") { return ( @@ -117,25 +115,40 @@ export function FrameAppDebuggerNotifications({ ); } - // @todo we should show a way to enable notifications - if (!notificationSettings) { - return ( - <> - - Notifications are not enabled - - In order debug the notifications you should allow the app to send - notifications. - - - - ); - } + const notificationsNotEnabled = !frameAppNotificationManager.state?.enabled; - // @todo add a way to send to webhook different events + // @todo on local dev we need to show an information that it won't be accessible to test notifications from non localhost frame apps + // @todo on production we need to show an information that localhost frame apps notifications can't be tested + // @todo show also events like frame add -> frame remove, notification enabled and disable in logs + response from frame app webhook so we can debug it return ( -
- +
+
+ +
+
+ {notifications.length === 0 ? ( +
+
+ + {!notificationsNotEnabled && ( + + )} +
+

+ {notificationsNotEnabled + ? "Notifications are not enabled" + : "No notifications"} +

+

+ {notificationsNotEnabled + ? "Notifications will appear here once they are enabled and the application sents any of them." + : "No notifications received yet."} +

+
+ ) : ( + + )} +
); } diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 5ce97a4c5..2891317ce 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -6,7 +6,6 @@ import { Card, CardContent } from "@/components/ui/card"; import { type FramePrimaryButton, type ResolveClientFunction, - type FrameClientConfig, useFrameAppInIframe, } from "@frames.js/render/use-frame-app"; import { useCallback, useRef, useState } from "react"; @@ -20,8 +19,11 @@ import { fallbackFrameContext } from "@frames.js/render"; import { Console } from "console-feed"; import type { Message } from "console-feed/lib/definitions/Component"; import type { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; -import type { NotificationSettings } from "../notifications/route"; import { FrameAppDebuggerNotifications } from "./frame-app-debugger-notifications"; +import { + FrameAppNotificationsManagerProvider, + useFrameAppNotificationsManager, +} from "../providers/FrameAppNotificationsManagerProvider"; type TabValues = "events" | "console" | "notifications"; @@ -30,7 +32,20 @@ type FrameAppDebuggerProps = { farcasterSigner: FarcasterSignerInstance; }; -const addFrameRequestsCache = new Set(); +// in debugger we don't want to automatically reject repeated add frame calls +const addFrameRequestsCache = new (class extends Set { + has(key: string) { + return false; + } + + add(key: string) { + return this; + } + + delete(key: string) { + return true; + } +})(); export function FrameAppDebugger({ context, @@ -38,10 +53,14 @@ export function FrameAppDebugger({ }: FrameAppDebuggerProps) { const farcasterSignerRef = useRef(farcasterSigner); farcasterSignerRef.current = farcasterSigner; + const frameAppNotificationManager = useFrameAppNotificationsManager({ + farcasterSigner, + context, + }); const { toast } = useToast(); const debuggerConsoleTabRef = useRef(null); const iframeRef = useRef(null); - const [activeTab, setActiveTab] = useState("events"); + const [activeTab, setActiveTab] = useState("notifications"); const [isAppReady, setIsAppReady] = useState(false); const [events, setEvents] = useState([]); const [primaryButton, setPrimaryButton] = useState<{ @@ -60,52 +79,32 @@ export function FrameAppDebugger({ ]); }, []); const resolveClient: ResolveClientFunction = useCallback(async () => { - const signer = farcasterSignerRef.current.signer; - let notificationDetails: FrameClientConfig["notificationDetails"]; - - if (signer?.status !== "approved") { - console.debug( - "Signer not approved, cannot resolve notification settings" - ); - } else { - const response = await fetch("/notifications", { - headers: { - "x-fid": signer.fid.toString(), - "x-frame-app-url": context.frame.button.action.url, - }, - }); + try { + const { manager } = await frameAppNotificationManager.promise; - if (response.status === 200) { - const data = await response.json(); + return { + clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), + added: !!manager.state, + notificationDetails: manager.state?.enabled + ? manager.state.details + : undefined, + }; + } catch (e) { + console.error(e); - notificationDetails = data; - } else if (response.status === 403) { - toast({ - title: "Access Forbidden", - description: "Debugger is probably misconfigured.", - variant: "destructive", - }); - } else if (response.status === 401) { - toast({ - title: "Not Authenticated", - description: "Check that you have approved farcaster signer.", - variant: "destructive", - }); - } + toast({ + title: "Unexpected error", + description: + "Failed to load notifications settings. Check the console for more details.", + variant: "destructive", + }); } return { clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), - added: !!notificationDetails, - notificationDetails, + added: false, }; - }, [context.frame.button.action.url, toast]); - /** - * Undefined means that it was never changed, so we should use the value from client - */ - const [notificationSettings, setNotificationSettings] = useState< - NotificationSettings["details"] | null | undefined - >(undefined); + }, [frameAppNotificationManager.promise, toast]); const frameApp = useFrameAppInIframe({ debug: true, source: context.parseResult, @@ -148,21 +147,28 @@ export function FrameAppDebugger({ }); }, async onAddFrameRequested(parseResult) { - // this is not compliant with how it should work in production - // but this is debugger, what if you want to test your function repeately - addFrameRequestsCache.clear(); - logEvent("info", "sdk.actions.addFrame() called"); - if (farcasterSigner.signer?.status !== "approved") { + if (frameAppNotificationManager.status === "pending") { toast({ - title: "Signer not approved", + title: "Notifications manager not ready", description: - "Signer is not approved. Approved farcaster signer is necessary.", + "Notifications manager is not ready. Please wait a moment.", variant: "destructive", }); - return false; + throw new Error("Notifications manager is not ready"); + } + + if (frameAppNotificationManager.status === "error") { + toast({ + title: "Notifications manager error", + description: + "Notifications manager failed to load. Please check the console for more details.", + variant: "destructive", + }); + + throw new Error("Notifications manager failed to load"); } const webhookUrl = parseResult.manifest?.manifest.frame?.webhookUrl; @@ -188,37 +194,26 @@ export function FrameAppDebugger({ return false; } - // register the settings - const response = await fetch("/notifications", { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify({ - fid: farcasterSigner.signer.fid, - frameUrl: context.frame.button.action.url, - webhookUrl, - }), - }); + try { + const result = + await frameAppNotificationManager.data.manager.addFrame(); + + return { + added: true, + notificationDetails: result, + }; + } catch (e) { + console.error(e); - if (response.status !== 201) { toast({ - title: "Failed to register notifications", - description: "Failed to register notifications for this app.", + title: "Failed to add frame", + description: + "Failed to add frame to the notifications manager. Check the console for more details.", variant: "destructive", }); - return false; + throw e; } - - const settings = (await response.json()) as NotificationSettings; - - setNotificationSettings(settings.details); - - return { - added: true, - notificationDetails: settings.details, - }; }, onDebugEthProviderRequest(parameters) { logEvent("info", "sdk.wallet.ethProvider.request() called", { @@ -315,51 +310,51 @@ export function FrameAppDebugger({
- {frameApp.status === "success" ? ( - - - setActiveTab(value as TabValues)} - className="grid grid-rows-[auto_1fr] w-full h-full" - > - - Events - Console - Notifications - - - - - + + + setActiveTab(value as TabValues)} + className="grid grid-rows-[auto_1fr] w-full h-full" > - { - if (debuggerConsoleTabRef.current) { - debuggerConsoleTabRef.current.scrollTo( - 0, - element.scrollHeight - ); - } - }} - /> - - - - - - - + + + Notifications + + Events + Console + + + + + + + + + { + if (debuggerConsoleTabRef.current) { + debuggerConsoleTabRef.current.scrollTo( + 0, + element.scrollHeight + ); + } + }} + /> + + + + + ) : null}
diff --git a/packages/debugger/app/components/frame-app-notifications-control-panel.tsx b/packages/debugger/app/components/frame-app-notifications-control-panel.tsx new file mode 100644 index 000000000..cb140130e --- /dev/null +++ b/packages/debugger/app/components/frame-app-notifications-control-panel.tsx @@ -0,0 +1,176 @@ +import { Switch } from "@/components/ui/switch"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import Link from "next/link"; +import { + ExternalLinkIcon, + InfoIcon, + Loader2Icon, + RefreshCwIcon, +} from "lucide-react"; +import { useFrameAppNotificationsManagerContext } from "../providers/FrameAppNotificationsManagerProvider"; +import { useCallback, useState } from "react"; +import { cn } from "@/lib/utils"; + +type FrameAppNotificationsControlPanelProps = {}; + +export function FrameAppNotificationsControlPanel({}: FrameAppNotificationsControlPanelProps) { + const [state, setState] = useState< + | "idle" + | "adding-frame" + | "removing-frame" + | "enabling-notifications" + | "disabling-notifications" + | "reloading-settings" + >("idle"); + const frameAppNotificationManager = useFrameAppNotificationsManagerContext(); + const isAddedToClient = !!frameAppNotificationManager.state; + const hasNotificationsEnabled = + isAddedToClient && !!frameAppNotificationManager.state.enabled; + + const addFrame = useCallback(async () => { + try { + setState("adding-frame"); + await frameAppNotificationManager.addFrame(); + } finally { + setState("idle"); + } + }, [frameAppNotificationManager]); + + const removeFrame = useCallback(async () => { + try { + setState("removing-frame"); + await frameAppNotificationManager.removeFrame(); + } finally { + setState("idle"); + } + }, [frameAppNotificationManager]); + + const reloadSettings = useCallback(async () => { + try { + setState("reloading-settings"); + + await frameAppNotificationManager.reload(); + } finally { + setState("idle"); + } + }, [frameAppNotificationManager]); + + const toggleNotifications = useCallback( + async (newValue: boolean) => { + try { + if (newValue) { + setState("enabling-notifications"); + await frameAppNotificationManager.enableNotifications(); + } else { + setState("disabling-notifications"); + await frameAppNotificationManager.disableNotifications(); + } + } finally { + setState("idle"); + } + }, + [frameAppNotificationManager] + ); + + return ( +
+

+ Client settings + +

+ {isAddedToClient ? ( + <> +
+ + +
+
+ +
+ + ) : ( + <> + + + Frame not added to client + + You need to add the frame to a client to be able to receive + notifications.{" "} + + See specification{" "} + + + . + + +
+ +
+ + )} +
+ ); +} diff --git a/packages/debugger/app/notifications/[id]/route.ts b/packages/debugger/app/notifications/[id]/route.ts index d0856b332..3cd126fb9 100644 --- a/packages/debugger/app/notifications/[id]/route.ts +++ b/packages/debugger/app/notifications/[id]/route.ts @@ -3,9 +3,10 @@ import { type SendNotificationRequest, type SendNotificationResponse, } from "@farcaster/frame-sdk"; -import { NextRequest } from "next/server"; +import type { NextRequest } from "next/server"; import { createRedis } from "../../lib/redis"; -import { NOTIFICATION_TTL_IN_SECONDS } from "../../constants"; +import { validateAuth } from "../auth"; +import { RedisNotificationsStorage } from "../storage"; export type NotificationUrl = { token: string; @@ -13,25 +14,31 @@ export type NotificationUrl = { export type Notification = Omit; -export async function POST(req: NextRequest): Promise { +/** + * Records notifications sent from frame app, marks them all as successful + */ +export async function POST( + req: NextRequest, + { params }: { params: { id: string } } +): Promise { const redis = createRedis(); - const notificationUrl = req.nextUrl.href; + const notificationsUrl = new URL( + `/notifications/${params.id}`, + req.nextUrl.href + ); const parseResult = sendNotificationRequestSchema.safeParse(await req.json()); if (!parseResult.success) { return Response.json(parseResult.error.flatten(), { status: 400 }); } - const settings = await redis.get(notificationUrl); - - if (!settings) { - return Response.json({ message: "Not Found" }, { status: 404 }); - } + const storage = new RedisNotificationsStorage({ + redis, + }); const { tokens, ...notification } = parseResult.data; - await redis.lpush(notificationUrl + ":list", notification); - await redis.expire(notificationUrl, NOTIFICATION_TTL_IN_SECONDS); + await storage.recordNotification(notificationsUrl.toString(), notification); return Response.json( { @@ -45,18 +52,54 @@ export async function POST(req: NextRequest): Promise { ); } -export async function GET(req: NextRequest): Promise { +/** + * Gets the recorded notifications + */ +export async function GET( + req: NextRequest, + { params }: { params: { id: string } } +): Promise { const redis = createRedis(); - const notificationUrl = req.nextUrl.href; + const notificationsUrl = new URL( + `/notifications/${params.id}`, + req.nextUrl.href + ); + const storage = new RedisNotificationsStorage({ + redis, + }); - const notifications = await redis.lpop( - notificationUrl + ":list", - 10 + const notifications = await storage.listNotifications( + notificationsUrl.toString() ); - return Response.json(notifications || [], { + return Response.json(notifications, { headers: { "Cache-Control": "no-store, no-cache", }, }); } + +/** + * Disables notifications for the given frame app + */ +export async function DELETE(req: NextRequest) { + const auth = validateAuth(req); + + if (!auth) { + return Response.json({ message: "Not Authenticated" }, { status: 401 }); + } + + const redis = createRedis(); + const storage = new RedisNotificationsStorage({ + redis, + }); + + // @todo call webhook with disable notifications event + + await storage.disableNotifications({ + fid: auth.fid, + frameAppUrl: auth.frameAppUrl, + }); + + return new Response(undefined, { status: 204 }); +} diff --git a/packages/debugger/app/notifications/auth.ts b/packages/debugger/app/notifications/auth.ts new file mode 100644 index 000000000..9e1106858 --- /dev/null +++ b/packages/debugger/app/notifications/auth.ts @@ -0,0 +1,13 @@ +export function validateAuth(req: Request) { + const fid = req.headers.get("x-fid"); + const frameAppUrl = req.headers.get("x-frame-url"); + + if (!fid || !frameAppUrl) { + return false; + } + + return { + fid, + frameAppUrl, + }; +} diff --git a/packages/debugger/app/notifications/helpers.ts b/packages/debugger/app/notifications/helpers.ts new file mode 100644 index 000000000..07c749d46 --- /dev/null +++ b/packages/debugger/app/notifications/helpers.ts @@ -0,0 +1,8 @@ +import type { NextRequest } from "next/server"; + +export function createNotificationsUrl(req: NextRequest): string { + return new URL( + `/notifications/${crypto.randomUUID()}`, + req.nextUrl.href + ).toString(); +} diff --git a/packages/debugger/app/notifications/route.ts b/packages/debugger/app/notifications/route.ts index b7d7a0c1c..6ac9d4edc 100644 --- a/packages/debugger/app/notifications/route.ts +++ b/packages/debugger/app/notifications/route.ts @@ -1,33 +1,10 @@ -import { FrameClientConfig } from "@frames.js/render/use-frame-app"; import type { NextRequest } from "next/server"; import { z } from "zod"; -import crypto from "node:crypto"; import { createRedis } from "../lib/redis"; -import type { NotificationUrl } from "./[id]/route"; -import { NOTIFICATION_TTL_IN_SECONDS } from "../constants"; - -function validateAuth(req: Request) { - const fid = req.headers.get("x-fid"); - const frameAppUrl = req.headers.get("x-frame-app-url"); - - if (!fid || !frameAppUrl) { - return false; - } - - return { - fid, - frameAppUrl, - }; -} - -function getFrameAppNotificationSettingsKey(fid: string, frameAppUrl: string) { - return `frame-app-notification:${fid}:${frameAppUrl}`; -} - -export type NotificationSettings = { - details: NonNullable; - webhookUrl: string; -}; +import { validateAuth } from "./auth"; +import type { NotificationSettings } from "./types"; +import { RedisNotificationsStorage } from "./storage"; +import { createNotificationsUrl } from "./helpers"; /** * Checks for the given notifications settings of user and url @@ -40,11 +17,14 @@ export async function GET(req: NextRequest): Promise { } const redis = createRedis(); + const storage = new RedisNotificationsStorage({ + redis, + }); - const { fid, frameAppUrl } = auth; - const key = getFrameAppNotificationSettingsKey(fid, frameAppUrl); - - const settings = await redis.get(key); + const settings = await storage.getSettings({ + fid: auth.fid, + frameAppUrl: auth.frameAppUrl, + }); if (!settings) { return Response.json({ message: "Not Found" }, { status: 404 }); @@ -57,49 +37,82 @@ export async function GET(req: NextRequest): Promise { }); } +export type CreateNotificationSettings = Extract< + NotificationSettings, + { enabled: true } +>; + const bodySchema = z.object({ - fid: z.coerce.number().int().min(1), - frameUrl: z.string().url(), webhookUrl: z.string().url(), }); /** - * Registers notification for the given url and user + * Adds frame app to client */ export async function POST(req: NextRequest): Promise { - const { fid, frameUrl, webhookUrl } = bodySchema.parse(await req.json()); + const auth = validateAuth(req); + + if (!auth) { + return Response.json({ message: "Not Authenticated" }, { status: 401 }); + } + + const { webhookUrl } = bodySchema.parse(await req.json()); const redis = createRedis(); - const key = getFrameAppNotificationSettingsKey(fid.toString(), frameUrl); - const token = crypto.randomUUID(); - const notificationUrl = new URL(`/notifications/${token}`, req.nextUrl.href); + const notificationsUrl = createNotificationsUrl(req); - const settings: NotificationSettings = { - webhookUrl, - details: { - token: crypto.randomUUID(), - url: notificationUrl.toString(), - }, - }; - - await redis.set( - notificationUrl.toString(), - { token }, - { - ex: NOTIFICATION_TTL_IN_SECONDS, - } - ); - const result = await redis.set(key, settings, { - ex: NOTIFICATION_TTL_IN_SECONDS, + const storage = new RedisNotificationsStorage({ + redis, + }); + + const existingSettings = await storage.getSettings({ + fid: auth.fid, + frameAppUrl: auth.frameAppUrl, }); - if (!result) { + if (existingSettings?.enabled) { return Response.json( - { message: "Failed to save settings" }, - { status: 500 } + { message: "Notifications are already enabled" }, + { status: 409 } ); } - // @todo call webhook? + const settings = await storage.addFrame({ + fid: auth.fid, + frameAppUrl: auth.frameAppUrl, + notificationsUrl, + webhookUrl, + }); + + if (existingSettings) { + // @todo call webhook with enable notifications event + } else { + // @todo call webhook with add frame event + } + + return Response.json(settings, { status: existingSettings ? 200 : 201 }); +} + +/** + * Removes frame app from client + */ +export async function DELETE(req: NextRequest): Promise { + const auth = validateAuth(req); + + if (!auth) { + return Response.json({ message: "Not Authenticated" }, { status: 401 }); + } + + const redis = createRedis(); + const storage = new RedisNotificationsStorage({ + redis, + }); + + // @todo call webhook with remove frame event + + await storage.removeFrame({ + fid: auth.fid, + frameAppUrl: auth.frameAppUrl, + }); - return Response.json(settings, { status: 201 }); + return new Response(undefined, { status: 204 }); } diff --git a/packages/debugger/app/notifications/storage.ts b/packages/debugger/app/notifications/storage.ts new file mode 100644 index 000000000..c43e5a6e5 --- /dev/null +++ b/packages/debugger/app/notifications/storage.ts @@ -0,0 +1,232 @@ +import type { FrameClientConfig } from "@frames.js/render/use-frame-app"; +import type { + Notification, + NotificationSettings, + NotificationsStorageInterface, +} from "./types"; +import { Redis } from "@upstash/redis"; +import crypto from "node:crypto"; +import { NOTIFICATION_TTL_IN_SECONDS } from "../constants"; + +type NotificationsEndpointSettings = { + token: string; + fid: string; + frameAppUrl: string; +}; + +export class RedisNotificationsStorage + implements NotificationsStorageInterface +{ + private redis: Redis; + + constructor({ redis }: { redis: Redis }) { + this.redis = redis; + } + + async getSettings(params: { + fid: string; + frameAppUrl: string; + }): Promise { + const settings = await this.redis.get( + createSettingsKey(params.fid, params.frameAppUrl) + ); + + if (!settings) { + return null; + } + + await this.redis.expire( + createSettingsKey(params.fid, params.frameAppUrl), + NOTIFICATION_TTL_IN_SECONDS + ); + + return settings; + } + + async addFrame({ + fid, + frameAppUrl, + notificationsUrl, + webhookUrl, + }: { + fid: string; + frameAppUrl: string; + webhookUrl: string; + notificationsUrl: string; + }) { + const settings: Extract = { + enabled: true, + details: { + token: crypto.randomUUID(), + url: notificationsUrl, + }, + webhookUrl, + }; + + await this.redis + .pipeline() + .set>( + createSettingsKey(fid, frameAppUrl), + settings + ) + .set( + createNotificationsEndpointSettingsKey(notificationsUrl), + { + fid, + frameAppUrl, + token: settings.details.token, + }, + { ex: NOTIFICATION_TTL_IN_SECONDS } + ) + .exec(); + + return settings; + } + + async removeFrame(params: { + fid: string; + frameAppUrl: string; + }): Promise { + const result = await this.redis.del( + createSettingsKey(params.fid, params.frameAppUrl) + ); + + if (!result) { + throw new Error("Failed to remove notification settings"); + } + } + + async disableNotifications({ + fid, + frameAppUrl, + }: { + fid: string; + frameAppUrl: string; + }): Promise { + const settings = await this.redis.get( + createSettingsKey(fid, frameAppUrl) + ); + + if (!settings) { + throw new Error("Notification settings not found"); + } + + if (!settings.enabled) { + throw new Error("Notifications are already disabled"); + } + + const newSettings: NotificationSettings = { + enabled: false, + webhookUrl: settings.webhookUrl, + }; + + await this.redis + .pipeline() + .set( + createSettingsKey(fid, frameAppUrl), + newSettings, + { ex: NOTIFICATION_TTL_IN_SECONDS } + ) + .del(createNotificationsEndpointSettingsKey(settings.details.url)) + .exec(); + } + + async recordNotification( + notificationsUrl: string, + notification: Notification + ): Promise { + const notificationsEndpointSettings = + await this.redis.get( + createNotificationsEndpointSettingsKey(notificationsUrl) + ); + + if (!notificationsEndpointSettings) { + throw new Error("Notifications endpoint settings not found"); + } + + const notificationSettings = await this.redis.get( + createSettingsKey( + notificationsEndpointSettings.fid, + notificationsEndpointSettings.frameAppUrl + ) + ); + + if (!notificationSettings) { + throw new Error("Frame is not added"); + } + + if (!notificationSettings.enabled) { + throw new Error("Notifications are disabled"); + } + + await this.redis + .pipeline() + .lpush( + createNotificationsListStorageKey(notificationSettings.details.url), + notification + ) + .expire( + createNotificationsListStorageKey(notificationSettings.details.url), + NOTIFICATION_TTL_IN_SECONDS + ) + .exec(); + + return; + } + + async listNotifications(notificationsUrl: string): Promise { + const notificationsEndpointSettings = + await this.redis.get( + createNotificationsEndpointSettingsKey(notificationsUrl) + ); + + if (!notificationsEndpointSettings) { + throw new Error("Notifications endpoint settings not found"); + } + + const notificationSettings = await this.redis.get( + createSettingsKey( + notificationsEndpointSettings.fid, + notificationsEndpointSettings.frameAppUrl + ) + ); + + if (!notificationSettings || !notificationSettings.enabled) { + return []; + } + + const notifications = await this.redis.lpop( + createNotificationsListStorageKey(notificationSettings.details.url), + 100 + ); + + await this.redis + .pipeline() + .expire( + createNotificationsEndpointSettingsKey(notificationsUrl), + NOTIFICATION_TTL_IN_SECONDS + ) + .expire( + createSettingsKey( + notificationsEndpointSettings.fid, + notificationsEndpointSettings.frameAppUrl + ), + NOTIFICATION_TTL_IN_SECONDS + ) + .exec(); + + return notifications || []; + } +} + +function createSettingsKey(fid: string, frameAppUrl: string) { + return `notification-settings:${fid}:${frameAppUrl}`; +} + +function createNotificationsEndpointSettingsKey(notificationsUrl: string) { + return `notifications-endpoint-settings:${notificationsUrl}`; +} + +function createNotificationsListStorageKey(notificationUrl: string) { + return `notifications-list:${notificationUrl}`; +} diff --git a/packages/debugger/app/notifications/types.ts b/packages/debugger/app/notifications/types.ts new file mode 100644 index 000000000..8fab25e9a --- /dev/null +++ b/packages/debugger/app/notifications/types.ts @@ -0,0 +1,38 @@ +import type { SendNotificationRequest } from "@farcaster/frame-sdk"; +import type { FrameClientConfig } from "@frames.js/render/use-frame-app"; + +export type Notification = Omit; + +export type NotificationSettings = + | { + enabled: true; + details: NonNullable; + webhookUrl: string; + } + | { + enabled: false; + webhookUrl: string; + }; + +export interface NotificationsStorageInterface { + addFrame(params: { + fid: string; + frameAppUrl: string; + notificationsUrl: string; + webhookUrl: string; + }): Promise>; + removeFrame(params: { fid: string; frameAppUrl: string }): Promise; + disableNotifications(params: { + fid: string; + frameAppUrl: string; + }): Promise; + recordNotification( + notificationsUrl: string, + notification: Notification + ): Promise; + listNotifications(notificationsUrl: string): Promise; + getSettings(params: { + fid: string; + frameAppUrl: string; + }): Promise; +} diff --git a/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx b/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx new file mode 100644 index 000000000..9dadeea7d --- /dev/null +++ b/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx @@ -0,0 +1,278 @@ +import { createContext, useContext } from "react"; +import type { CreateNotificationSettings } from "../notifications/route"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; +import { FrameLaunchedInContext } from "../components/frame-debugger"; +import type { NotificationSettings } from "../notifications/types"; + +export const notificationManagerQueryKeys = { + settingsQuery: (fid: string, frameAppUrl: string): string[] => [ + "notification-settings", + fid, + frameAppUrl, + ], +}; + +export type FrameAppNotificationsManager = { + readonly state: NotificationSettings | null | undefined; + addFrame(): Promise< + Extract["details"] + >; + removeFrame(): Promise; + enableNotifications(): Promise; + disableNotifications(): Promise; + reload(): Promise; +}; + +const FrameAppNotificationsManagerContext = + createContext({ + state: null, + async addFrame() { + throw new Error("Not implemented"); + }, + async removeFrame() {}, + async enableNotifications() {}, + async disableNotifications() {}, + async reload() {}, + }); + +type FrameAppNotificationsManagerProviderProps = { + children: React.ReactNode; + manager: FrameAppNotificationsManager; +}; + +export function FrameAppNotificationsManagerProvider({ + children, + manager, +}: FrameAppNotificationsManagerProviderProps) { + return ( + + {children} + + ); +} + +type UseFrameAppNotificationsManagerOptions = { + context: FrameLaunchedInContext; + farcasterSigner: FarcasterSignerInstance; +}; + +type UseFrameAppNotificationsManagerResult = { + manager: FrameAppNotificationsManager; +}; + +export function useFrameAppNotificationsManager({ + context, + farcasterSigner, +}: UseFrameAppNotificationsManagerOptions) { + const { signer } = farcasterSigner; + const frameUrl = context.frame.button.action.url; + + const queryClient = useQueryClient(); + + const addFrameMutation = useMutation({ + async mutationFn() { + if (signer?.status !== "approved") { + throw new Error("Signer not approved"); + } + + const webhookUrl = + context.parseResult.manifest?.manifest.frame?.webhookUrl; + + if (!webhookUrl) { + throw new Error("Webhook URL not found"); + } + + const response = await fetch("/notifications", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-fid": signer.fid.toString(), + "x-frame-url": frameUrl, + }, + body: JSON.stringify({ + webhookUrl, + }), + }); + + if (response.status !== 201) { + throw new Error("Failed to add frame"); + } + + const data = (await response.json()) as CreateNotificationSettings; + + return data["details"]; + }, + }); + + const removeFrameMutation = useMutation({ + async mutationFn() { + if (signer?.status !== "approved") { + throw new Error("Signer not approved"); + } + + const response = await fetch("/notifications", { + method: "DELETE", + headers: { + "x-fid": signer.fid.toString(), + "x-frame-url": frameUrl, + }, + }); + + if (response.status === 204) { + return true; + } + + throw new Error("Failed to remove frame"); + }, + }); + + const enableNotificationsMutation = useMutation({ + async mutationFn() { + if (signer?.status !== "approved") { + throw new Error("Signer not approved"); + } + + const webhookUrl = + context.parseResult.manifest?.manifest.frame?.webhookUrl; + + if (!webhookUrl) { + throw new Error("Webhook URL not found"); + } + + const response = await fetch("/notifications", { + method: "POST", + headers: { + "Content-Type": "application/json", + "x-fid": signer.fid.toString(), + "x-frame-url": frameUrl, + }, + body: JSON.stringify({ + webhookUrl, + }), + }); + + if (response.status !== 201 && response.status !== 200) { + throw new Error("Failed to enable notifications"); + } + }, + }); + + const disableNotificationsMutation = useMutation({ + async mutationFn({ notificationUrl }: { notificationUrl: string }) { + if (signer?.status !== "approved") { + throw new Error("Signer not approved"); + } + + const response = await fetch(notificationUrl, { + method: "DELETE", + headers: { + "Content-Type": "application/json", + "x-fid": signer.fid.toString(), + "x-frame-url": frameUrl, + }, + }); + + if (response.status === 204) { + return; + } + + throw new Error("Failed to disable notifications"); + }, + }); + + return useQuery({ + experimental_prefetchInRender: true, + queryKey: [ + "frame-app-notifications-manager", + signer?.status === "approved" ? signer.fid.toString() : null, + context.frame.button.action.url, + ], + async queryFn({ queryKey }) { + if (signer?.status !== "approved") { + throw new Error("Signer not approved"); + } + + const response = await fetch("/notifications", { + method: "GET", + headers: { + "x-fid": signer.fid.toString(), + "x-frame-url": frameUrl, + }, + }); + + return { + manager: { + state: response.ok + ? ((await response.json()) as NotificationSettings) + : null, + async addFrame() { + const result = await addFrameMutation.mutateAsync(); + + // refetch notification settings + await queryClient.refetchQueries({ + queryKey, + }); + + return result; + }, + async removeFrame() { + if (!this.state) { + throw new Error("Frame not added"); + } + + await removeFrameMutation.mutateAsync(); + + // refetch notification settings + await queryClient.refetchQueries({ + queryKey, + }); + }, + async enableNotifications() { + if (!this.state) { + throw new Error("Frame not added"); + } + + if (this.state.enabled === true) { + throw new Error("Notifications already enabled"); + } + + await enableNotificationsMutation.mutateAsync(); + + // refetch notification settings + await queryClient.refetchQueries({ + queryKey, + }); + }, + async disableNotifications() { + if (!this.state) { + throw new Error("Frame not added"); + } + + if (this.state.enabled === false) { + throw new Error("Notifications already disabled"); + } + + await disableNotificationsMutation.mutateAsync({ + notificationUrl: this.state.details.url, + }); + + // refetch notification settings + await queryClient.refetchQueries({ + queryKey, + }); + }, + reload() { + return queryClient.refetchQueries({ + queryKey, + }); + }, + }, + }; + }, + }); +} + +export function useFrameAppNotificationsManagerContext() { + return useContext(FrameAppNotificationsManagerContext); +} diff --git a/packages/debugger/package.json b/packages/debugger/package.json index bdece7669..07b8bf5cd 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -17,7 +17,7 @@ }, "dependencies": { "@upstash/redis": "^1.34.3", - "@lens-protocol/client": "2.3.2", + "@lens-protocol/client": "^2.3.2", "@farcaster/frame-sdk": "^0.0.16", "@xmtp/xmtp-js": "^12.0.0", "is-port-reachable": "^4.0.0", @@ -50,7 +50,7 @@ "@radix-ui/react-tooltip": "^1.0.7", "@rainbow-me/rainbowkit": "^2.1.2", "@reservoir0x/reservoir-sdk": "^2.0.11", - "@tanstack/react-query": "^5.22.2", + "@tanstack/react-query": "^5.62.7", "@types/node": "^18.17.0", "@types/react": "^18.2.0", "@types/react-dom": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 04f661442..80ec4c7b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2821,7 +2821,7 @@ ethers "^5.7.2" tslib "^2.6.2" -"@lens-protocol/client@2.3.2", "@lens-protocol/client@^2.3.2": +"@lens-protocol/client@^2.3.2": version "2.3.2" resolved "https://registry.yarnpkg.com/@lens-protocol/client/-/client-2.3.2.tgz#9421308bb691374c20a44d9784cee33d6d8a7b25" integrity sha512-TOgCCTcJHHUI8dyfiAFo1rz8SukLgpZ+YFOf/34MsdPuAreK236MVAeIjXKFVnxLBxeLFcxCmUaBjsI0WD5eoQ== @@ -5440,17 +5440,17 @@ dependencies: "@swc/counter" "^0.1.3" -"@tanstack/query-core@5.28.6": - version "5.28.6" - resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.28.6.tgz#a3bdb108f9f8d4e2ba3163068dbe6ff55b905a81" - integrity sha512-hnhotV+DnQtvtR3jPvbQMPNMW4KEK0J4k7c609zJ8muiNknm+yoDyMHmxTWM5ZnlZpsz0zOxYFr+mzRJNHWJsA== +"@tanstack/query-core@5.62.7": + version "5.62.7" + resolved "https://registry.yarnpkg.com/@tanstack/query-core/-/query-core-5.62.7.tgz#c7f6d0131c08cd2f60e73ec6e7b70e2e9e335def" + integrity sha512-fgpfmwatsrUal6V+8EC2cxZIQVl9xvL7qYa03gsdsCy985UTUlS4N+/3hCzwR0PclYDqisca2AqR1BVgJGpUDA== -"@tanstack/react-query@^5.22.2": - version "5.28.6" - resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.28.6.tgz#0d52b0a98a1d842debf9c65496e20a9981a23bc4" - integrity sha512-/DdYuDBSsA21Qbcder1R8Cr/3Nx0ZnA2lgtqKsLMvov8wL4+g0HBz/gWYZPlIsof7iyfQafyhg4wUVUsS3vWZw== +"@tanstack/react-query@^5.62.7": + version "5.62.7" + resolved "https://registry.yarnpkg.com/@tanstack/react-query/-/react-query-5.62.7.tgz#8f253439a38ad6ce820bc6d42d89ca2556574d1a" + integrity sha512-+xCtP4UAFDTlRTYyEjLx0sRtWyr5GIk7TZjZwBu4YaNahi3Rt2oMyRqfpfVrtwsqY2sayP4iXVCwmC+ZqqFmuw== dependencies: - "@tanstack/query-core" "5.28.6" + "@tanstack/query-core" "5.62.7" "@types/acorn@^4.0.0": version "4.0.6" From 8eb885edb8090a7eea52e00520090e472e0c68f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 18 Dec 2024 11:18:35 +0100 Subject: [PATCH 67/88] feat: notifications event log and webhooks --- .../frame-app-debugger-notifications.tsx | 101 +++-- .../app/components/frame-app-debugger.tsx | 54 ++- .../frame-app-notifications-control-panel.tsx | 41 +- .../debugger/app/notifications/[id]/route.ts | 105 ------ .../[namespaceId]/events/route.ts | 58 +++ .../app/notifications/[namespaceId]/route.ts | 351 ++++++++++++++++++ .../notifications/[namespaceId]/send/route.ts | 55 +++ packages/debugger/app/notifications/auth.ts | 13 - packages/debugger/app/notifications/route.ts | 137 ++----- .../debugger/app/notifications/storage.ts | 324 ++++++++-------- packages/debugger/app/notifications/types.ts | 58 +-- .../FrameAppNotificationsManagerProvider.tsx | 224 ++++------- packages/debugger/package.json | 2 +- packages/frames.js/package.json | 11 + packages/frames.js/src/farcaster-v2/events.ts | 61 +++ packages/render/package.json | 4 +- yarn.lock | 34 +- 17 files changed, 991 insertions(+), 642 deletions(-) delete mode 100644 packages/debugger/app/notifications/[id]/route.ts create mode 100644 packages/debugger/app/notifications/[namespaceId]/events/route.ts create mode 100644 packages/debugger/app/notifications/[namespaceId]/route.ts create mode 100644 packages/debugger/app/notifications/[namespaceId]/send/route.ts delete mode 100644 packages/debugger/app/notifications/auth.ts create mode 100644 packages/frames.js/src/farcaster-v2/events.ts diff --git a/packages/debugger/app/components/frame-app-debugger-notifications.tsx b/packages/debugger/app/components/frame-app-debugger-notifications.tsx index 5e6fc67d6..2f48034a6 100644 --- a/packages/debugger/app/components/frame-app-debugger-notifications.tsx +++ b/packages/debugger/app/components/frame-app-debugger-notifications.tsx @@ -1,13 +1,15 @@ import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Console } from "console-feed"; -import { InboxIcon, Loader2Icon } from "lucide-react"; +import { InboxIcon, Loader2Icon, TrashIcon } from "lucide-react"; import type { ParseFramesV2ResultWithFrameworkDetails } from "frames.js/frame-parsers"; import { useEffect, useState } from "react"; import { Message } from "console-feed/lib/definitions/Component"; -import type { Notification } from "../notifications/[id]/route"; import { useQuery } from "@tanstack/react-query"; import { FrameAppNotificationsControlPanel } from "./frame-app-notifications-control-panel"; import { useFrameAppNotificationsManagerContext } from "../providers/FrameAppNotificationsManagerProvider"; +import type { GETEventsResponseBody } from "../notifications/[namespaceId]/events/route"; +import { Button } from "@/components/ui/button"; +import { WithTooltip } from "./with-tooltip"; type FrameAppDebuggerNotificationsProps = { frame: ParseFramesV2ResultWithFrameworkDetails; @@ -17,23 +19,21 @@ export function FrameAppDebuggerNotifications({ frame, }: FrameAppDebuggerNotificationsProps) { const frameAppNotificationManager = useFrameAppNotificationsManagerContext(); - const [notifications, setNotifications] = useState([]); + const [events, setEvents] = useState([]); const notificationsQuery = useQuery({ initialData: [], - enabled: frameAppNotificationManager.state?.enabled ?? false, + enabled: !!frameAppNotificationManager.state?.namespaceUrl, queryKey: [ "frame-app-notifications-log", - frameAppNotificationManager.state?.enabled - ? frameAppNotificationManager.state.details.url - : null, + frameAppNotificationManager.state?.namespaceUrl, ], async queryFn() { - if (!frameAppNotificationManager.state?.enabled) { + if (!frameAppNotificationManager.state?.namespaceUrl) { return [] as Message[]; } const response = await fetch( - frameAppNotificationManager.state.details.url, + `${frameAppNotificationManager.state.namespaceUrl}/events`, { method: "GET", } @@ -43,14 +43,44 @@ export function FrameAppDebuggerNotifications({ return [] as Message[]; } - const data = (await response.json()) as Notification[]; + const events = (await response.json()) as GETEventsResponseBody; - return data.map((notification): Message => { - return { - method: "log", - id: notification.notificationId, - data: ["Received notification", notification], - }; + return events.map((event): Message => { + switch (event.type) { + case "notification": + return { + method: "log", + id: crypto.randomUUID(), + data: ["🔔 Received notification", event.notification], + }; + case "event": + return { + method: "info", + id: crypto.randomUUID(), + data: ["➡️ Send event", event], + }; + case "event_failure": { + return { + method: "error", + id: crypto.randomUUID(), + data: ["❗ Received invalid response for event", event], + }; + } + case "event_success": { + return { + method: "log", + id: crypto.randomUUID(), + data: ["✅ Received successful response for event", event], + }; + } + default: + event as never; + return { + method: "error", + id: crypto.randomUUID(), + data: ["Received unknown event", event], + }; + } }); }, refetchInterval: 5000, @@ -58,7 +88,7 @@ export function FrameAppDebuggerNotifications({ useEffect(() => { if (notificationsQuery.data) { - setNotifications((prev) => [...prev, ...notificationsQuery.data]); + setEvents((prev) => [...prev, ...notificationsQuery.data]); } }, [notificationsQuery.data]); @@ -115,40 +145,51 @@ export function FrameAppDebuggerNotifications({ ); } - const notificationsNotEnabled = !frameAppNotificationManager.state?.enabled; + const notificationsEnabled = + frameAppNotificationManager.state?.frame.status === "added" && + !!frameAppNotificationManager.state.frame.notificationDetails; - // @todo on local dev we need to show an information that it won't be accessible to test notifications from non localhost frame apps - // @todo on production we need to show an information that localhost frame apps notifications can't be tested - // @todo show also events like frame add -> frame remove, notification enabled and disable in logs + response from frame app webhook so we can debug it return (
-
- {notifications.length === 0 ? ( + {events.length === 0 ? ( +
- {!notificationsNotEnabled && ( + {!notificationsEnabled && ( )}

- {notificationsNotEnabled + {!notificationsEnabled ? "Notifications are not enabled" : "No notifications"}

- {notificationsNotEnabled + {!notificationsEnabled ? "Notifications will appear here once they are enabled and the application sents any of them." : "No notifications received yet."}

- ) : ( - - )} -
+
+ ) : ( +
+

+ Event log + + + +

+
+ +
+
+ )}
); } diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 2891317ce..10449e4a5 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -16,8 +16,6 @@ import { cn } from "@/lib/utils"; import { DebuggerConsole } from "./debugger-console"; import Image from "next/image"; import { fallbackFrameContext } from "@frames.js/render"; -import { Console } from "console-feed"; -import type { Message } from "console-feed/lib/definitions/Component"; import type { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; import { FrameAppDebuggerNotifications } from "./frame-app-debugger-notifications"; import { @@ -62,32 +60,28 @@ export function FrameAppDebugger({ const iframeRef = useRef(null); const [activeTab, setActiveTab] = useState("notifications"); const [isAppReady, setIsAppReady] = useState(false); - const [events, setEvents] = useState([]); const [primaryButton, setPrimaryButton] = useState<{ button: FramePrimaryButton; callback: () => void; } | null>(null); const provider = useWagmiProvider(); - const logEvent = useCallback((method: Message["method"], ...args: any[]) => { - setEvents((prev) => [ - ...prev, - { - id: prev.length.toString(), - method, - data: args, - }, - ]); - }, []); const resolveClient: ResolveClientFunction = useCallback(async () => { try { const { manager } = await frameAppNotificationManager.promise; + const clientFid = parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"); + + if (!manager.state || manager.state.frame.status === "removed") { + return { + clientFid, + added: false, + }; + } return { clientFid: parseInt(process.env.FARCASTER_DEVELOPER_FID ?? "-1"), - added: !!manager.state, - notificationDetails: manager.state?.enabled - ? manager.state.details - : undefined, + added: true, + notificationDetails: + manager.state.frame.notificationDetails ?? undefined, }; } catch (e) { console.error(e); @@ -123,31 +117,32 @@ export function FrameAppDebugger({ proxyUrl: "/frames", addFrameRequestsCache, onReady(options) { - logEvent("info", "sdk.actions.ready() called", { options }); + console.info("sdk.actions.ready() called", { options }); setIsAppReady(true); }, onClose() { - logEvent("info", "sdk.actions.close() called"); + console.info("sdk.actions.close() called"); toast({ title: "Frame app closed", description: "The frame app called close() action.", }); }, onOpenUrl(url) { - logEvent("info", "sdk.actions.openUrl() called", { url }); + console.info("sdk.actions.openUrl() called", { url }); window.open(url, "_blank"); }, onPrimaryButtonSet(button, buttonCallback) { - logEvent("info", "sdk.actions.setPrimaryButton() called", { button }); + console.info("sdk.actions.setPrimaryButton() called", { button }); setPrimaryButton({ button, callback: () => { + console.info("primary button clicked"); buttonCallback(); }, }); }, async onAddFrameRequested(parseResult) { - logEvent("info", "sdk.actions.addFrame() called"); + console.info("sdk.actions.addFrame() called"); if (frameAppNotificationManager.status === "pending") { toast({ @@ -216,9 +211,7 @@ export function FrameAppDebugger({ } }, onDebugEthProviderRequest(parameters) { - logEvent("info", "sdk.wallet.ethProvider.request() called", { - parameters, - }); + console.info("sdk.wallet.ethProvider.request() called", { parameters }); }, }); @@ -322,19 +315,18 @@ export function FrameAppDebugger({ onValueChange={(value) => setActiveTab(value as TabValues)} className="grid grid-rows-[auto_1fr] w-full h-full" > - + Notifications - Events Console - + - - - ("idle"); const frameAppNotificationManager = useFrameAppNotificationsManagerContext(); - const isAddedToClient = !!frameAppNotificationManager.state; + const isAddedToClient = + frameAppNotificationManager.state?.frame.status === "added"; const hasNotificationsEnabled = - isAddedToClient && !!frameAppNotificationManager.state.enabled; + frameAppNotificationManager.state?.frame.status === "added" && + !!frameAppNotificationManager.state.frame.notificationDetails; const addFrame = useCallback(async () => { try { @@ -75,23 +78,25 @@ export function FrameAppNotificationsControlPanel({}: FrameAppNotificationsContr ); return ( -
+

Client settings - + + +

{isAddedToClient ? ( <> @@ -106,7 +111,7 @@ export function FrameAppNotificationsControlPanel({}: FrameAppNotificationsContr htmlFor="notifications-enabled" className="inline-flex gap-2 items-center" > - Notifications enabled{" "} + Notifications{" "} {state === "disabling-notifications" || state === "enabling-notifications" ? ( diff --git a/packages/debugger/app/notifications/[id]/route.ts b/packages/debugger/app/notifications/[id]/route.ts deleted file mode 100644 index 3cd126fb9..000000000 --- a/packages/debugger/app/notifications/[id]/route.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { - sendNotificationRequestSchema, - type SendNotificationRequest, - type SendNotificationResponse, -} from "@farcaster/frame-sdk"; -import type { NextRequest } from "next/server"; -import { createRedis } from "../../lib/redis"; -import { validateAuth } from "../auth"; -import { RedisNotificationsStorage } from "../storage"; - -export type NotificationUrl = { - token: string; -}; - -export type Notification = Omit; - -/** - * Records notifications sent from frame app, marks them all as successful - */ -export async function POST( - req: NextRequest, - { params }: { params: { id: string } } -): Promise { - const redis = createRedis(); - const notificationsUrl = new URL( - `/notifications/${params.id}`, - req.nextUrl.href - ); - const parseResult = sendNotificationRequestSchema.safeParse(await req.json()); - - if (!parseResult.success) { - return Response.json(parseResult.error.flatten(), { status: 400 }); - } - - const storage = new RedisNotificationsStorage({ - redis, - }); - - const { tokens, ...notification } = parseResult.data; - - await storage.recordNotification(notificationsUrl.toString(), notification); - - return Response.json( - { - result: { - successfulTokens: tokens, - invalidTokens: [], - rateLimitedTokens: [], - }, - } satisfies SendNotificationResponse, - { status: 200 } - ); -} - -/** - * Gets the recorded notifications - */ -export async function GET( - req: NextRequest, - { params }: { params: { id: string } } -): Promise { - const redis = createRedis(); - const notificationsUrl = new URL( - `/notifications/${params.id}`, - req.nextUrl.href - ); - const storage = new RedisNotificationsStorage({ - redis, - }); - - const notifications = await storage.listNotifications( - notificationsUrl.toString() - ); - - return Response.json(notifications, { - headers: { - "Cache-Control": "no-store, no-cache", - }, - }); -} - -/** - * Disables notifications for the given frame app - */ -export async function DELETE(req: NextRequest) { - const auth = validateAuth(req); - - if (!auth) { - return Response.json({ message: "Not Authenticated" }, { status: 401 }); - } - - const redis = createRedis(); - const storage = new RedisNotificationsStorage({ - redis, - }); - - // @todo call webhook with disable notifications event - - await storage.disableNotifications({ - fid: auth.fid, - frameAppUrl: auth.frameAppUrl, - }); - - return new Response(undefined, { status: 204 }); -} diff --git a/packages/debugger/app/notifications/[namespaceId]/events/route.ts b/packages/debugger/app/notifications/[namespaceId]/events/route.ts new file mode 100644 index 000000000..21d555362 --- /dev/null +++ b/packages/debugger/app/notifications/[namespaceId]/events/route.ts @@ -0,0 +1,58 @@ +import type { NextRequest } from "next/server"; +import { createRedis } from "../../../lib/redis"; +import { RedisNotificationsStorage } from "../../storage"; +import { z } from "zod"; +import { + eventPayloadSchema, + sendNotificationRequestSchema, +} from "@farcaster/frame-sdk"; + +const getEventsResponseBodySchema = z.array( + z.discriminatedUnion("type", [ + z.object({ + id: z.string().uuid(), + type: z.literal("notification"), + notification: sendNotificationRequestSchema, + }), + z.object({ + id: z.string().uuid(), + type: z.literal("event"), + event: eventPayloadSchema, + }), + z.object({ + id: z.string().uuid(), + type: z.literal("event_success"), + eventId: z.string().uuid(), + event: eventPayloadSchema, + }), + z.object({ + id: z.string().uuid(), + type: z.literal("event_failure"), + event: eventPayloadSchema, + eventId: z.string().uuid(), + message: z.string(), + response: z + .object({ + status: z.number(), + headers: z.record(z.union([z.string(), z.array(z.string())])), + body: z.string(), + }) + .optional(), + }), + ]) +); + +export type GETEventsResponseBody = z.infer; + +export async function GET( + req: NextRequest, + { params }: { params: { namespaceId: string } } +) { + const redis = createRedis(); + const storage = new RedisNotificationsStorage(redis, req.nextUrl.href); + const events = await storage.listEvents(params.namespaceId); + + return Response.json( + getEventsResponseBodySchema.parse(events satisfies GETEventsResponseBody) + ); +} diff --git a/packages/debugger/app/notifications/[namespaceId]/route.ts b/packages/debugger/app/notifications/[namespaceId]/route.ts new file mode 100644 index 000000000..808d660bc --- /dev/null +++ b/packages/debugger/app/notifications/[namespaceId]/route.ts @@ -0,0 +1,351 @@ +import { + notificationDetailsSchema, + type SendNotificationRequest, +} from "@farcaster/frame-sdk"; +import type { NextRequest } from "next/server"; +import { createRedis } from "../../lib/redis"; +import { RedisNotificationsStorage } from "../storage"; +import { z } from "zod"; +import { + FrameEvent, + InvalidWebhookResponseError, + sendEvent, +} from "frames.js/farcaster-v2/events"; +import { type Hex, hexToBytes } from "viem"; + +export type NotificationUrl = { + token: string; +}; + +export type Notification = Omit; + +const postRequestBodySchema = z.discriminatedUnion("action", [ + z.object({ + action: z.literal("add_frame"), + }), + z.object({ + action: z.literal("remove_frame"), + }), + z.object({ + action: z.literal("enable_notifications"), + }), + z.object({ + action: z.literal("disable_notifications"), + }), +]); + +export type POSTNotificationsDetailRequestBody = z.infer< + typeof postRequestBodySchema +>; + +const postResponseBodySchema = z.discriminatedUnion("type", [ + z.object({ + type: z.literal("frame_added"), + notificationDetails: notificationDetailsSchema, + }), + z.object({ + type: z.literal("frame_removed"), + }), + z.object({ + type: z.literal("notifications_enabled"), + notificationDetails: notificationDetailsSchema, + }), + z.object({ + type: z.literal("notifications_disabled"), + }), +]); + +export type POSTNotificationsDetailResponseBody = z.infer< + typeof postResponseBodySchema +>; + +/** + * Handles different operations on namespace level + */ +export async function POST( + req: NextRequest, + { params }: { params: { namespaceId: string } } +) { + const requestBody = postRequestBodySchema.safeParse(await req.json()); + + if (!requestBody.success) { + return Response.json(requestBody.error.flatten(), { status: 400 }); + } + + const redis = createRedis(); + const storage = new RedisNotificationsStorage(redis, req.nextUrl.href); + const namespace = await storage.getNamespace(params.namespaceId); + + if (!namespace) { + return Response.json({ message: "Not found" }, { status: 404 }); + } + + const privateKey = hexToBytes(namespace.signerPrivateKey as Hex); + + switch (requestBody.data.action) { + case "add_frame": { + const notificationDetails = await storage.addFrame(namespace); + + const eventId = crypto.randomUUID(); + await storage.recordEvent(namespace.id, { + type: "event", + event: { + event: "frame_added", + notificationDetails, + }, + id: eventId, + }); + + const event: FrameEvent = { + event: "frame_added", + notificationDetails, + }; + + sendEvent(event, { + privateKey, + fid: namespace.fid, + webhookUrl: namespace.webhookUrl, + }) + .then(() => { + return storage.recordEvent(namespace.id, { + type: "event_success", + event, + eventId, + id: crypto.randomUUID(), + }); + }) + .catch(async (e) => { + return storage.recordEvent(namespace.id, { + type: "event_failure", + event, + eventId, + id: crypto.randomUUID(), + message: e instanceof Error ? e.message : String(e), + response: + e instanceof InvalidWebhookResponseError + ? { + body: await e.response.text(), + headers: Object.fromEntries(e.response.headers.entries()), + status: e.response.status, + } + : undefined, + }); + }); + + return Response.json( + postResponseBodySchema.parse({ + type: "frame_added", + notificationDetails, + }), + { status: 201 } + ); + } + case "remove_frame": { + await storage.removeFrame(namespace); + + const eventId = crypto.randomUUID(); + const event: FrameEvent = { + event: "frame_removed", + }; + + await storage.recordEvent(namespace.id, { + type: "event", + event, + id: eventId, + }); + + sendEvent(event, { + fid: namespace.fid, + privateKey, + webhookUrl: namespace.webhookUrl, + }) + .then(() => { + return storage.recordEvent(namespace.id, { + type: "event_success", + event, + eventId, + id: crypto.randomUUID(), + }); + }) + .catch(async (e) => { + return storage.recordEvent(namespace.id, { + type: "event_failure", + event, + eventId, + id: crypto.randomUUID(), + message: e instanceof Error ? e.message : String(e), + response: + e instanceof InvalidWebhookResponseError + ? { + body: await e.response.text(), + headers: Object.fromEntries(e.response.headers.entries()), + status: e.response.status, + } + : undefined, + }); + }); + + return Response.json( + postResponseBodySchema.parse({ type: "frame_removed" }), + { + status: 200, + } + ); + } + case "enable_notifications": { + const notificationDetails = await storage.enableNotifications(namespace); + + const eventId = crypto.randomUUID(); + await storage.recordEvent(namespace.id, { + type: "event", + event: { + event: "notifications_enabled", + notificationDetails, + }, + id: eventId, + }); + + const event: FrameEvent = { + event: "notifications_enabled", + notificationDetails, + }; + + sendEvent(event, { + fid: namespace.fid, + privateKey, + webhookUrl: namespace.webhookUrl, + }) + .then(() => { + return storage.recordEvent(namespace.id, { + type: "event_success", + event, + eventId, + id: crypto.randomUUID(), + }); + }) + .catch(async (e) => { + return storage.recordEvent(namespace.id, { + type: "event_failure", + event, + eventId, + id: crypto.randomUUID(), + message: e instanceof Error ? e.message : String(e), + response: + e instanceof InvalidWebhookResponseError + ? { + body: await e.response.text(), + headers: Object.fromEntries(e.response.headers.entries()), + status: e.response.status, + } + : undefined, + }); + }); + + return Response.json( + postResponseBodySchema.parse({ + type: "notifications_enabled", + notificationDetails, + }), + { status: 201 } + ); + } + case "disable_notifications": { + await storage.disableNotifications(namespace); + + const eventId = crypto.randomUUID(); + await storage.recordEvent(namespace.id, { + type: "event", + event: { + event: "notifications_disabled", + }, + id: eventId, + }); + + const event: FrameEvent = { + event: "notifications_disabled", + }; + + sendEvent(event, { + fid: namespace.fid, + privateKey, + webhookUrl: namespace.webhookUrl, + }) + .then(() => { + return storage.recordEvent(namespace.id, { + type: "event_success", + event, + eventId, + id: crypto.randomUUID(), + }); + }) + .catch(async (e) => { + return storage.recordEvent(namespace.id, { + type: "event_failure", + event, + eventId, + id: crypto.randomUUID(), + message: e instanceof Error ? e.message : String(e), + response: + e instanceof InvalidWebhookResponseError + ? { + body: await e.response.text(), + headers: Object.fromEntries(e.response.headers.entries()), + status: e.response.status, + } + : undefined, + }); + }); + + return Response.json( + postResponseBodySchema.parse({ type: "notifications_disabled" }), + { + status: 200, + } + ); + } + default: { + requestBody.data satisfies never; + throw new Error(`Unknown action`); + } + } +} + +export const getResponseBodySchema = z.object({ + fid: z.number(), + frameAppUrl: z.string().url(), + namespaceUrl: z.string().url(), + webhookUrl: z.string().url(), + frame: z.discriminatedUnion("status", [ + z.object({ + status: z.literal("added"), + notificationDetails: notificationDetailsSchema.nullable(), + }), + z.object({ + status: z.literal("removed"), + }), + ]), +}); + +export type GETNotificationsDetailResponseBody = z.infer< + typeof getResponseBodySchema +>; + +/** + * Gets the namespace settings + */ +export async function GET( + req: NextRequest, + { params }: { params: { namespaceId: string } } +): Promise { + const redis = createRedis(); + const storage = new RedisNotificationsStorage(redis, req.nextUrl.href); + const namespace = await storage.getNamespace(params.namespaceId); + + if (!namespace) { + return Response.json({ message: "Not found" }, { status: 404 }); + } + + return Response.json(getResponseBodySchema.parse(namespace), { + status: 200, + }); +} diff --git a/packages/debugger/app/notifications/[namespaceId]/send/route.ts b/packages/debugger/app/notifications/[namespaceId]/send/route.ts new file mode 100644 index 000000000..b3b5f656a --- /dev/null +++ b/packages/debugger/app/notifications/[namespaceId]/send/route.ts @@ -0,0 +1,55 @@ +import type { NextRequest } from "next/server"; +import { createRedis } from "../../../lib/redis"; +import { RedisNotificationsStorage } from "../../storage"; +import { + sendNotificationRequestSchema, + sendNotificationResponseSchema, + SendNotificationResponse, +} from "@farcaster/frame-sdk"; +import crypto from "node:crypto"; + +export async function POST( + req: NextRequest, + { params }: { params: { namespaceId: string } } +) { + const requestBody = sendNotificationRequestSchema.safeParse(await req.json()); + + if (!requestBody.success) { + return Response.json(requestBody.error.flatten(), { status: 400 }); + } + + const redis = createRedis(); + const storage = new RedisNotificationsStorage(redis, req.nextUrl.href); + const namespace = await storage.getNamespace(params.namespaceId); + + if ( + !namespace || + namespace.frame.status !== "added" || + !namespace.frame.notificationDetails + ) { + return Response.json({ error: "Not found" }, { status: 404 }); + } + + await storage.recordEvent(params.namespaceId, { + type: "notification", + notification: requestBody.data, + id: crypto.randomUUID(), + }); + + const notificationDetails = namespace.frame.notificationDetails; + const tokens = requestBody.data.tokens; + + return Response.json( + sendNotificationResponseSchema.parse({ + result: { + invalidTokens: tokens.filter( + (token) => token !== notificationDetails.token + ), + rateLimitedTokens: [], + successfulTokens: tokens.filter( + (token) => token === notificationDetails.token + ), + }, + } satisfies SendNotificationResponse) + ); +} diff --git a/packages/debugger/app/notifications/auth.ts b/packages/debugger/app/notifications/auth.ts deleted file mode 100644 index 9e1106858..000000000 --- a/packages/debugger/app/notifications/auth.ts +++ /dev/null @@ -1,13 +0,0 @@ -export function validateAuth(req: Request) { - const fid = req.headers.get("x-fid"); - const frameAppUrl = req.headers.get("x-frame-url"); - - if (!fid || !frameAppUrl) { - return false; - } - - return { - fid, - frameAppUrl, - }; -} diff --git a/packages/debugger/app/notifications/route.ts b/packages/debugger/app/notifications/route.ts index 6ac9d4edc..632306479 100644 --- a/packages/debugger/app/notifications/route.ts +++ b/packages/debugger/app/notifications/route.ts @@ -1,118 +1,59 @@ import type { NextRequest } from "next/server"; import { z } from "zod"; import { createRedis } from "../lib/redis"; -import { validateAuth } from "./auth"; -import type { NotificationSettings } from "./types"; import { RedisNotificationsStorage } from "./storage"; -import { createNotificationsUrl } from "./helpers"; - -/** - * Checks for the given notifications settings of user and url - */ -export async function GET(req: NextRequest): Promise { - const auth = validateAuth(req); - - if (!auth) { - return Response.json({ message: "Not Authenticated" }, { status: 401 }); - } - - const redis = createRedis(); - const storage = new RedisNotificationsStorage({ - redis, - }); - - const settings = await storage.getSettings({ - fid: auth.fid, - frameAppUrl: auth.frameAppUrl, - }); - - if (!settings) { - return Response.json({ message: "Not Found" }, { status: 404 }); - } - - return Response.json(settings, { - headers: { - "Cache-Control": "no-store, no-cache", - }, - }); -} - -export type CreateNotificationSettings = Extract< - NotificationSettings, - { enabled: true } ->; - -const bodySchema = z.object({ +import { + type GETNotificationsDetailResponseBody, + getResponseBodySchema, +} from "./[namespaceId]/route"; + +const postRequestBodySchema = z.object({ + fid: z.coerce.number().int().positive(), + frameAppUrl: z.string().url(), + signerPrivateKey: z.string().min(1), webhookUrl: z.string().url(), }); -/** - * Adds frame app to client - */ -export async function POST(req: NextRequest): Promise { - const auth = validateAuth(req); - - if (!auth) { - return Response.json({ message: "Not Authenticated" }, { status: 401 }); - } - - const { webhookUrl } = bodySchema.parse(await req.json()); - const redis = createRedis(); - const notificationsUrl = createNotificationsUrl(req); - - const storage = new RedisNotificationsStorage({ - redis, - }); - - const existingSettings = await storage.getSettings({ - fid: auth.fid, - frameAppUrl: auth.frameAppUrl, - }); - - if (existingSettings?.enabled) { - return Response.json( - { message: "Notifications are already enabled" }, - { status: 409 } - ); - } - - const settings = await storage.addFrame({ - fid: auth.fid, - frameAppUrl: auth.frameAppUrl, - notificationsUrl, - webhookUrl, - }); - - if (existingSettings) { - // @todo call webhook with enable notifications event - } else { - // @todo call webhook with add frame event - } +export type POSTNotificationsRequestBody = z.infer< + typeof postRequestBodySchema +>; - return Response.json(settings, { status: existingSettings ? 200 : 201 }); -} +export type POSTNotificationsResponseBody = GETNotificationsDetailResponseBody; /** - * Removes frame app from client + * Creates new notification settings namespace. + * + * This endpoint should be used to initialize notification settings namespace + * because it returns a unique URL that is used then to set settings and list all + * recorded events. */ -export async function DELETE(req: NextRequest): Promise { - const auth = validateAuth(req); +export async function POST(req: NextRequest) { + const body = postRequestBodySchema.safeParse(await req.json()); - if (!auth) { - return Response.json({ message: "Not Authenticated" }, { status: 401 }); + if (!body.success) { + return Response.json(body.error.flatten(), { status: 400 }); } const redis = createRedis(); - const storage = new RedisNotificationsStorage({ - redis, - }); + const { fid, frameAppUrl, signerPrivateKey, webhookUrl } = body.data; + const storage = new RedisNotificationsStorage(redis, req.nextUrl.href); - // @todo call webhook with remove frame event + // Create new namespace + const namespaceId = crypto.randomUUID(); - await storage.removeFrame({ - fid: auth.fid, - frameAppUrl: auth.frameAppUrl, + const namespace = await storage.registerNamespace(namespaceId, { + fid, + frameAppUrl, + signerPrivateKey, + webhookUrl, }); - return new Response(undefined, { status: 204 }); + return Response.json( + getResponseBodySchema.parse( + namespace + ) satisfies POSTNotificationsResponseBody, + { + status: 201, + } + ); } diff --git a/packages/debugger/app/notifications/storage.ts b/packages/debugger/app/notifications/storage.ts index c43e5a6e5..1800e20b5 100644 --- a/packages/debugger/app/notifications/storage.ts +++ b/packages/debugger/app/notifications/storage.ts @@ -1,232 +1,218 @@ -import type { FrameClientConfig } from "@frames.js/render/use-frame-app"; -import type { - Notification, - NotificationSettings, - NotificationsStorageInterface, -} from "./types"; +import type { RecordedEvent } from "./types"; import { Redis } from "@upstash/redis"; import crypto from "node:crypto"; import { NOTIFICATION_TTL_IN_SECONDS } from "../constants"; +import type { FrameNotificationDetails } from "@farcaster/frame-sdk"; -type NotificationsEndpointSettings = { - token: string; - fid: string; +type NotificationsNamespace = { + id: string; + fid: number; frameAppUrl: string; + signerPrivateKey: string; + namespaceUrl: string; + webhookUrl: string; + frame: + | { + status: "added"; + notificationDetails: null | FrameNotificationDetails; + } + | { + status: "removed"; + }; }; -export class RedisNotificationsStorage - implements NotificationsStorageInterface -{ - private redis: Redis; +export class RedisNotificationsStorage { + constructor( + private redis: Redis, + private serverUrl: string + ) {} + + async registerNamespace( + id: string, + params: { + fid: number; + frameAppUrl: string; + signerPrivateKey: string; + webhookUrl: string; + } + ) { + const namespace: NotificationsNamespace = { + id, + fid: params.fid, + frameAppUrl: params.frameAppUrl, + signerPrivateKey: params.signerPrivateKey, + namespaceUrl: new URL(`/notifications/${id}`, this.serverUrl).toString(), + webhookUrl: params.webhookUrl, + frame: { + status: "removed", + }, + }; - constructor({ redis }: { redis: Redis }) { - this.redis = redis; + await this.redis.set( + createNamespaceKey(id), + namespace, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } + ); + + return namespace; } - async getSettings(params: { - fid: string; - frameAppUrl: string; - }): Promise { - const settings = await this.redis.get( - createSettingsKey(params.fid, params.frameAppUrl) + async getNamespace(id: string): Promise { + const namespace = await this.redis.get( + createNamespaceKey(id) ); - if (!settings) { + if (!namespace) { return null; } await this.redis.expire( - createSettingsKey(params.fid, params.frameAppUrl), + createNamespaceKey(id), NOTIFICATION_TTL_IN_SECONDS ); - return settings; + return namespace; } - async addFrame({ - fid, - frameAppUrl, - notificationsUrl, - webhookUrl, - }: { - fid: string; - frameAppUrl: string; - webhookUrl: string; - notificationsUrl: string; - }) { - const settings: Extract = { - enabled: true, - details: { - token: crypto.randomUUID(), - url: notificationsUrl, - }, - webhookUrl, + async addFrame(namespace: NotificationsNamespace) { + if (namespace.frame.status === "added") { + throw new Error("Frame is already added"); + } + + const token = crypto.randomUUID(); + const url = new URL( + `/notifications/${namespace.id}/send`, + this.serverUrl + ).toString(); + const notificationDetails: FrameNotificationDetails = { + url, + token, }; - await this.redis - .pipeline() - .set>( - createSettingsKey(fid, frameAppUrl), - settings - ) - .set( - createNotificationsEndpointSettingsKey(notificationsUrl), - { - fid, - frameAppUrl, - token: settings.details.token, + await this.redis.set( + createNamespaceKey(namespace.id), + { + ...namespace, + frame: { + status: "added", + notificationDetails, }, - { ex: NOTIFICATION_TTL_IN_SECONDS } - ) - .exec(); + }, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } + ); - return settings; + return notificationDetails; } - async removeFrame(params: { - fid: string; - frameAppUrl: string; - }): Promise { - const result = await this.redis.del( - createSettingsKey(params.fid, params.frameAppUrl) - ); - - if (!result) { - throw new Error("Failed to remove notification settings"); + async removeFrame(namespace: NotificationsNamespace) { + if (namespace.frame.status === "removed") { + throw new Error("Frame is already removed"); } - } - async disableNotifications({ - fid, - frameAppUrl, - }: { - fid: string; - frameAppUrl: string; - }): Promise { - const settings = await this.redis.get( - createSettingsKey(fid, frameAppUrl) + await this.redis.set( + createNamespaceKey(namespace.id), + { + ...namespace, + frame: { + status: "removed", + }, + }, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } ); + } - if (!settings) { - throw new Error("Notification settings not found"); - } - - if (!settings.enabled) { - throw new Error("Notifications are already disabled"); + async enableNotifications(namespace: NotificationsNamespace) { + if (namespace.frame.status === "removed") { + throw new Error("Frame is not added"); } - const newSettings: NotificationSettings = { - enabled: false, - webhookUrl: settings.webhookUrl, + const token = crypto.randomUUID(); + const url = new URL( + `/notifications/${namespace.id}/send`, + this.serverUrl + ).toString(); + const notificationDetails: FrameNotificationDetails = { + token, + url, }; - await this.redis - .pipeline() - .set( - createSettingsKey(fid, frameAppUrl), - newSettings, - { ex: NOTIFICATION_TTL_IN_SECONDS } - ) - .del(createNotificationsEndpointSettingsKey(settings.details.url)) - .exec(); + await this.redis.set( + createNamespaceKey(namespace.id), + { + ...namespace, + frame: { + status: "added", + notificationDetails, + }, + }, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } + ); + + return notificationDetails; } - async recordNotification( - notificationsUrl: string, - notification: Notification - ): Promise { - const notificationsEndpointSettings = - await this.redis.get( - createNotificationsEndpointSettingsKey(notificationsUrl) - ); - - if (!notificationsEndpointSettings) { - throw new Error("Notifications endpoint settings not found"); + async disableNotifications(namespace: NotificationsNamespace) { + if (namespace.frame.status === "removed") { + throw new Error("Frame is not added"); } - const notificationSettings = await this.redis.get( - createSettingsKey( - notificationsEndpointSettings.fid, - notificationsEndpointSettings.frameAppUrl - ) + await this.redis.set( + createNamespaceKey(namespace.id), + { + ...namespace, + frame: { + status: "added", + notificationDetails: null, + }, + }, + { + ex: NOTIFICATION_TTL_IN_SECONDS, + } ); + } - if (!notificationSettings) { - throw new Error("Frame is not added"); - } - - if (!notificationSettings.enabled) { - throw new Error("Notifications are disabled"); - } + async recordEvent(namespaceId: string, event: RecordedEvent): Promise { + const eventListStorageKey = createEventListStorageKey(namespaceId); await this.redis .pipeline() - .lpush( - createNotificationsListStorageKey(notificationSettings.details.url), - notification - ) - .expire( - createNotificationsListStorageKey(notificationSettings.details.url), - NOTIFICATION_TTL_IN_SECONDS - ) + .rpush(eventListStorageKey, event) + .expire(eventListStorageKey, NOTIFICATION_TTL_IN_SECONDS) .exec(); return; } - async listNotifications(notificationsUrl: string): Promise { - const notificationsEndpointSettings = - await this.redis.get( - createNotificationsEndpointSettingsKey(notificationsUrl) - ); - - if (!notificationsEndpointSettings) { - throw new Error("Notifications endpoint settings not found"); - } - - const notificationSettings = await this.redis.get( - createSettingsKey( - notificationsEndpointSettings.fid, - notificationsEndpointSettings.frameAppUrl - ) - ); + async listEvents(namespaceId: string): Promise { + const namespace = await this.getNamespace(namespaceId); - if (!notificationSettings || !notificationSettings.enabled) { + if (!namespace || namespace.frame.status === "removed") { return []; } - const notifications = await this.redis.lpop( - createNotificationsListStorageKey(notificationSettings.details.url), + const eventListStorageKey = createEventListStorageKey(namespaceId); + + const notifications = await this.redis.lpop( + eventListStorageKey, 100 ); - await this.redis - .pipeline() - .expire( - createNotificationsEndpointSettingsKey(notificationsUrl), - NOTIFICATION_TTL_IN_SECONDS - ) - .expire( - createSettingsKey( - notificationsEndpointSettings.fid, - notificationsEndpointSettings.frameAppUrl - ), - NOTIFICATION_TTL_IN_SECONDS - ) - .exec(); - return notifications || []; } } -function createSettingsKey(fid: string, frameAppUrl: string) { - return `notification-settings:${fid}:${frameAppUrl}`; -} - -function createNotificationsEndpointSettingsKey(notificationsUrl: string) { - return `notifications-endpoint-settings:${notificationsUrl}`; +function createEventListStorageKey(namespaceId: string) { + return `notifications-namespace:${namespaceId}:events`; } -function createNotificationsListStorageKey(notificationUrl: string) { - return `notifications-list:${notificationUrl}`; +function createNamespaceKey(namespaceId: string) { + return `notifications-namespace:${namespaceId}`; } diff --git a/packages/debugger/app/notifications/types.ts b/packages/debugger/app/notifications/types.ts index 8fab25e9a..6ba965fdb 100644 --- a/packages/debugger/app/notifications/types.ts +++ b/packages/debugger/app/notifications/types.ts @@ -1,38 +1,48 @@ import type { SendNotificationRequest } from "@farcaster/frame-sdk"; import type { FrameClientConfig } from "@frames.js/render/use-frame-app"; +import type { FrameEvent } from "frames.js/farcaster-v2/events"; -export type Notification = Omit; +export type Notification = SendNotificationRequest; + +export type RecordedEvent = + | { + type: "notification"; + notification: Notification; + id: string; + } + | { + type: "event"; + event: FrameEvent; + id: string; + } + | { + type: "event_success"; + event: FrameEvent; + id: string; + eventId: string; + } + | { + type: "event_failure"; + event: FrameEvent; + id: string; + eventId: string; + message: string; + response?: { + status: number; + headers: Record; + body: string; + }; + }; export type NotificationSettings = | { enabled: true; details: NonNullable; webhookUrl: string; + signerPrivateKey: string; } | { enabled: false; webhookUrl: string; + signerPrivateKey: string; }; - -export interface NotificationsStorageInterface { - addFrame(params: { - fid: string; - frameAppUrl: string; - notificationsUrl: string; - webhookUrl: string; - }): Promise>; - removeFrame(params: { fid: string; frameAppUrl: string }): Promise; - disableNotifications(params: { - fid: string; - frameAppUrl: string; - }): Promise; - recordNotification( - notificationsUrl: string, - notification: Notification - ): Promise; - listNotifications(notificationsUrl: string): Promise; - getSettings(params: { - fid: string; - frameAppUrl: string; - }): Promise; -} diff --git a/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx b/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx index 9dadeea7d..b2fef6eed 100644 --- a/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx +++ b/packages/debugger/app/providers/FrameAppNotificationsManagerProvider.tsx @@ -1,9 +1,17 @@ -import { createContext, useContext } from "react"; -import type { CreateNotificationSettings } from "../notifications/route"; +import { createContext, useContext, useRef } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; -import { FrameLaunchedInContext } from "../components/frame-debugger"; +import type { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; +import type { FrameLaunchedInContext } from "../components/frame-debugger"; import type { NotificationSettings } from "../notifications/types"; +import type { + POSTNotificationsRequestBody, + POSTNotificationsResponseBody, +} from "../notifications/route"; +import type { + GETNotificationsDetailResponseBody, + POSTNotificationsDetailRequestBody, + POSTNotificationsDetailResponseBody, +} from "../notifications/[namespaceId]/route"; export const notificationManagerQueryKeys = { settingsQuery: (fid: string, frameAppUrl: string): string[] => [ @@ -14,7 +22,7 @@ export const notificationManagerQueryKeys = { }; export type FrameAppNotificationsManager = { - readonly state: NotificationSettings | null | undefined; + readonly state: GETNotificationsDetailResponseBody | undefined; addFrame(): Promise< Extract["details"] >; @@ -26,7 +34,7 @@ export type FrameAppNotificationsManager = { const FrameAppNotificationsManagerContext = createContext({ - state: null, + state: undefined, async addFrame() { throw new Error("Not implemented"); }, @@ -65,119 +73,30 @@ export function useFrameAppNotificationsManager({ context, farcasterSigner, }: UseFrameAppNotificationsManagerOptions) { + const namespaceSettingsRef = useRef( + null + ); const { signer } = farcasterSigner; const frameUrl = context.frame.button.action.url; - + const webhookUrl = context.parseResult.manifest?.manifest.frame?.webhookUrl; const queryClient = useQueryClient(); - const addFrameMutation = useMutation({ - async mutationFn() { - if (signer?.status !== "approved") { - throw new Error("Signer not approved"); - } - - const webhookUrl = - context.parseResult.manifest?.manifest.frame?.webhookUrl; - - if (!webhookUrl) { - throw new Error("Webhook URL not found"); - } - - const response = await fetch("/notifications", { - method: "POST", - headers: { - "Content-Type": "application/json", - "x-fid": signer.fid.toString(), - "x-frame-url": frameUrl, - }, - body: JSON.stringify({ - webhookUrl, - }), - }); - - if (response.status !== 201) { - throw new Error("Failed to add frame"); - } - - const data = (await response.json()) as CreateNotificationSettings; - - return data["details"]; - }, - }); - - const removeFrameMutation = useMutation({ - async mutationFn() { - if (signer?.status !== "approved") { - throw new Error("Signer not approved"); - } - - const response = await fetch("/notifications", { - method: "DELETE", - headers: { - "x-fid": signer.fid.toString(), - "x-frame-url": frameUrl, - }, - }); - - if (response.status === 204) { - return true; - } - - throw new Error("Failed to remove frame"); - }, - }); - - const enableNotificationsMutation = useMutation({ - async mutationFn() { - if (signer?.status !== "approved") { - throw new Error("Signer not approved"); - } - - const webhookUrl = - context.parseResult.manifest?.manifest.frame?.webhookUrl; - - if (!webhookUrl) { - throw new Error("Webhook URL not found"); + const sendEvent = useMutation({ + async mutationFn(event: POSTNotificationsDetailRequestBody) { + if (!namespaceSettingsRef.current) { + throw new Error("Namespace settings not found"); } - const response = await fetch("/notifications", { + const response = await fetch(namespaceSettingsRef.current.namespaceUrl, { method: "POST", - headers: { - "Content-Type": "application/json", - "x-fid": signer.fid.toString(), - "x-frame-url": frameUrl, - }, - body: JSON.stringify({ - webhookUrl, - }), + body: JSON.stringify(event), }); if (response.status !== 201 && response.status !== 200) { throw new Error("Failed to enable notifications"); } - }, - }); - - const disableNotificationsMutation = useMutation({ - async mutationFn({ notificationUrl }: { notificationUrl: string }) { - if (signer?.status !== "approved") { - throw new Error("Signer not approved"); - } - const response = await fetch(notificationUrl, { - method: "DELETE", - headers: { - "Content-Type": "application/json", - "x-fid": signer.fid.toString(), - "x-frame-url": frameUrl, - }, - }); - - if (response.status === 204) { - return; - } - - throw new Error("Failed to disable notifications"); + return response.json() as Promise; }, }); @@ -193,35 +112,70 @@ export function useFrameAppNotificationsManager({ throw new Error("Signer not approved"); } - const response = await fetch("/notifications", { - method: "GET", - headers: { - "x-fid": signer.fid.toString(), - "x-frame-url": frameUrl, - }, - }); + if (!webhookUrl) { + throw new Error("Webhook URL not found"); + } + + let state: POSTNotificationsResponseBody; + + if (!namespaceSettingsRef.current) { + const response = await fetch("/notifications", { + method: "POST", + body: JSON.stringify({ + fid: signer.fid, + frameAppUrl: frameUrl, + signerPrivateKey: signer.privateKey, + webhookUrl, + } satisfies POSTNotificationsRequestBody), + }); + + if (response.status !== 201) { + throw new Error("Failed to create notification settings"); + } + + state = (await response.json()) as POSTNotificationsResponseBody; + + namespaceSettingsRef.current = state; + } else { + const response = await fetch( + namespaceSettingsRef.current.namespaceUrl, + { + method: "GET", + } + ); + + if (!response.ok) { + namespaceSettingsRef.current = null; + + throw new Error("Failed to fetch notification settings"); + } + + state = (await response.json()) as POSTNotificationsResponseBody; + } return { manager: { - state: response.ok - ? ((await response.json()) as NotificationSettings) - : null, + state: state as GETNotificationsDetailResponseBody, async addFrame() { - const result = await addFrameMutation.mutateAsync(); + const result = await sendEvent.mutateAsync({ + action: "add_frame", + }); + + if (result.type !== "frame_added") { + throw new Error("Failed to add frame"); + } // refetch notification settings await queryClient.refetchQueries({ queryKey, }); - return result; + return result.notificationDetails; }, async removeFrame() { - if (!this.state) { - throw new Error("Frame not added"); - } - - await removeFrameMutation.mutateAsync(); + await sendEvent.mutateAsync({ + action: "remove_frame", + }); // refetch notification settings await queryClient.refetchQueries({ @@ -229,15 +183,9 @@ export function useFrameAppNotificationsManager({ }); }, async enableNotifications() { - if (!this.state) { - throw new Error("Frame not added"); - } - - if (this.state.enabled === true) { - throw new Error("Notifications already enabled"); - } - - await enableNotificationsMutation.mutateAsync(); + await sendEvent.mutateAsync({ + action: "enable_notifications", + }); // refetch notification settings await queryClient.refetchQueries({ @@ -245,16 +193,8 @@ export function useFrameAppNotificationsManager({ }); }, async disableNotifications() { - if (!this.state) { - throw new Error("Frame not added"); - } - - if (this.state.enabled === false) { - throw new Error("Notifications already disabled"); - } - - await disableNotificationsMutation.mutateAsync({ - notificationUrl: this.state.details.url, + await sendEvent.mutateAsync({ + action: "disable_notifications", }); // refetch notification settings diff --git a/packages/debugger/package.json b/packages/debugger/package.json index 07b8bf5cd..ad7b83704 100644 --- a/packages/debugger/package.json +++ b/packages/debugger/package.json @@ -18,7 +18,7 @@ "dependencies": { "@upstash/redis": "^1.34.3", "@lens-protocol/client": "^2.3.2", - "@farcaster/frame-sdk": "^0.0.16", + "@farcaster/frame-sdk": "^0.0.18", "@xmtp/xmtp-js": "^12.0.0", "is-port-reachable": "^4.0.0", "next": "14.1.4", diff --git a/packages/frames.js/package.json b/packages/frames.js/package.json index 8db420d6b..62bef84e0 100644 --- a/packages/frames.js/package.json +++ b/packages/frames.js/package.json @@ -157,6 +157,16 @@ "default": "./dist/anonymous/index.cjs" } }, + "./farcaster-v2/events": { + "import": { + "types": "./dist/farcaster-v2/events.d.ts", + "default": "./dist/farcaster-v2/events.js" + }, + "require": { + "types": "./dist/farcaster-v2/events.d.cts", + "default": "./dist/farcaster-v2/events.cjs" + } + }, "./farcaster-v2/json-signature": { "import": { "types": "./dist/farcaster-v2/json-signature.d.ts", @@ -409,6 +419,7 @@ "react-dom": "^18.2.0" }, "dependencies": { + "@farcaster/frame-node": "^0.0.4", "@vercel/og": "^0.6.3", "cheerio": "^1.0.0-rc.12", "protobufjs": "^7.2.6", diff --git a/packages/frames.js/src/farcaster-v2/events.ts b/packages/frames.js/src/farcaster-v2/events.ts new file mode 100644 index 000000000..42f1068a3 --- /dev/null +++ b/packages/frames.js/src/farcaster-v2/events.ts @@ -0,0 +1,61 @@ +import type { + EncodedJsonFarcasterSignatureSchema, + FrameEvent, +} from "@farcaster/frame-node"; +import { + createJsonFarcasterSignature, + eventPayloadSchema, +} from "@farcaster/frame-node"; + +export class InvalidWebhookResponseError extends Error { + constructor( + public statusCode: number, + public response: Response + ) { + super("Invalid webhook response"); + } +} + +export type { FrameEvent }; + +type SendEventOptions = { + /** + * App private key + */ + privateKey: string | Uint8Array; + fid: number; + webhookUrl: string | URL; +}; + +/** + * Sends an event to frame app webhook. + */ +export async function sendEvent( + event: FrameEvent, + { privateKey, fid, webhookUrl }: SendEventOptions +): Promise { + const payload = eventPayloadSchema.parse(event); + const signature = createJsonFarcasterSignature({ + fid, + payload: Buffer.from(JSON.stringify(payload)), + privateKey: Buffer.from(privateKey), + type: "app_key", + }); + + const response = await fetch(webhookUrl, { + method: "POST", + cache: "no-cache", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify( + signature satisfies EncodedJsonFarcasterSignatureSchema + ), + }); + + if (response.status >= 200 && response.status < 300) { + return; + } + + throw new InvalidWebhookResponseError(response.status, response); +} diff --git a/packages/render/package.json b/packages/render/package.json index e2fae6eee..55eaceed6 100644 --- a/packages/render/package.json +++ b/packages/render/package.json @@ -316,7 +316,7 @@ "src" ], "devDependencies": { - "@farcaster/frame-sdk": "^0.0.16", + "@farcaster/frame-sdk": "^0.0.18", "@lens-protocol/client": "^2.3.2", "@rainbow-me/rainbowkit": "^2.1.2", "@remix-run/node": "^2.8.1", @@ -332,7 +332,7 @@ }, "license": "MIT", "peerDependencies": { - "@farcaster/frame-sdk": "^0.0.16", + "@farcaster/frame-sdk": "^0.0.18", "@lens-protocol/client": "^2.0.0", "@rainbow-me/rainbowkit": "^2.1.2", "@types/react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 80ec4c7b9..ce2e2a033 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2397,20 +2397,36 @@ neverthrow "^6.0.0" viem "^1.12.2" -"@farcaster/frame-core@^0.0.15": - version "0.0.15" - resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.15.tgz#05a4ed6f7c0d43d2f41d13714fb8b13419068a11" - integrity sha512-WQfAEqyQAz3EzEdfqAMV7s2VMIYBGWz0Qt5CUUkmSelvv0a+8A61YmBnpemCi3NEwWzEJBTc/IxzQ29w2axPBg== +"@farcaster/frame-core@^0.0.16": + version "0.0.16" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.16.tgz#c84ded4470a2fb17e4abec3fb26dcfc10b8f083f" + integrity sha512-BnFfXigvN9E4mKuMk94d0EWZbcu5KEgPSRjjc9SC5RVPkZzM/UqewRXSALzfl0+ENx0dBU7gIoha7XN2RsLyvQ== dependencies: ox "^0.4.0" zod "^3.23.8" -"@farcaster/frame-sdk@^0.0.16": - version "0.0.16" - resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.16.tgz#1e3dc191f950065d2f38daf61dc45775d7073421" - integrity sha512-043j2EiCOaHvS1ox0yVx7KZQU5LB1P+39waK3xtX0knNKQ54wZxCOTcca0kUcJfTBN5db7iVPOh/E+LJ5kjDew== +"@farcaster/frame-core@^0.0.17": + version "0.0.17" + resolved "https://registry.yarnpkg.com/@farcaster/frame-core/-/frame-core-0.0.17.tgz#e679dfba507486296c6b36e2fca04a53440853c2" + integrity sha512-AJnUfuTrZCc2OP7OkpdIfrus2zscnxo+6ph5WzUhVUH66UM6NPAoqhbWR15PRODTdyR2TdDz7aNq/0PlxiAiwg== + dependencies: + ox "^0.4.0" + zod "^3.23.8" + +"@farcaster/frame-node@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@farcaster/frame-node/-/frame-node-0.0.4.tgz#450a131f898ec120f45d335adf1689a1ad84d153" + integrity sha512-3MOou30EE+ItAE43nWchwRlylKL98jy5DmD757M0EupV6v0mc58cU6VsBbWtL6PiVMunq8N5nVuGFJwWyvDJHQ== + dependencies: + "@farcaster/frame-core" "^0.0.16" + ox "^0.4.0" + +"@farcaster/frame-sdk@^0.0.18": + version "0.0.18" + resolved "https://registry.yarnpkg.com/@farcaster/frame-sdk/-/frame-sdk-0.0.18.tgz#3ddde95fe7e201a1e7a1bf685ba13011b37bdd32" + integrity sha512-aUe2+bopQgGHZBu1GGDBA2jD9pqhx2ljvRh4y3Z3g/riuIBJM+jgx6j6pF6MlUKneAYNkPc3byRXfzX5SBpLDA== dependencies: - "@farcaster/frame-core" "^0.0.15" + "@farcaster/frame-core" "^0.0.17" comlink "^4.4.2" eventemitter3 "^5.0.1" ox "^0.4.0" From cfae0400dc5531a9289bfba183fc2d6f1beec902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 18 Dec 2024 11:41:05 +0100 Subject: [PATCH 68/88] chore: use bigger ttl --- packages/debugger/app/constants.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/debugger/app/constants.ts b/packages/debugger/app/constants.ts index 6884fa0a0..fb8c90e21 100644 --- a/packages/debugger/app/constants.ts +++ b/packages/debugger/app/constants.ts @@ -2,4 +2,5 @@ export const LOCAL_STORAGE_KEYS = { SELECTED_PROTOCOL: "selectedProtocol", }; -export const NOTIFICATION_TTL_IN_SECONDS = 10; +// 8 hours +export const NOTIFICATION_TTL_IN_SECONDS = 8 * 60 * 60; From e4f0a05c596eb570752b726683cc62c208fc89ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Wed, 18 Dec 2024 11:44:33 +0100 Subject: [PATCH 69/88] chore: add cli options for kv store --- packages/debugger/.env.sample | 2 -- packages/debugger/bin/debugger.js | 20 ++++++++++++++++++++ packages/debugger/globals.d.ts | 4 +++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/packages/debugger/.env.sample b/packages/debugger/.env.sample index 95c873a3a..001b3deb9 100644 --- a/packages/debugger/.env.sample +++ b/packages/debugger/.env.sample @@ -11,7 +11,5 @@ FARCASTER_DEVELOPER_FID= NEXT_PUBLIC_WALLETCONNECT_ID= # Required to debug Farcaster Frames v2 notifications -KV_URL="" -KV_REST_API_READ_ONLY_TOKEN="" KV_REST_API_TOKEN="" KV_REST_API_URL="" \ No newline at end of file diff --git a/packages/debugger/bin/debugger.js b/packages/debugger/bin/debugger.js index 619a3f96b..6c799b709 100755 --- a/packages/debugger/bin/debugger.js +++ b/packages/debugger/bin/debugger.js @@ -26,6 +26,18 @@ const args = yargs(hideBin(process.argv)) default: process.env.FARCASTER_DEVELOPER_ID || process.env.FARCASTER_DEVELOPER_FID, }) + .option("kv-rest-api-url", { + alias: "kvurl", + type: "string", + description: "Needed for debugging Farcaster Frames v2 events", + default: process.env.KV_REST_API_URL, + }) + .option("kv-rest-api-token", { + alias: "kvtoken", + type: "string", + description: "Needed for debugging Farcaster Frames v2 events", + default: process.env.KV_REST_API_TOKEN, + }) .option("signer-url", { type: "string", description: @@ -54,6 +66,14 @@ if (args["farcaster-developer-fid"]) { process.env.FARCASTER_DEVELOPER_ID = args["farcaster-developer-fid"]; } +if (args["kv-rest-api-url"]) { + process.env.KV_REST_API_URL = args["kv-rest-api-url"]; +} + +if (args["kv-rest-api-token"]) { + process.env.KV_REST_API_TOKEN = args["kv-rest-api-token"]; +} + if (args["signer-url"]) { process.env.SIGNER_URL = args["signer-url"]; } diff --git a/packages/debugger/globals.d.ts b/packages/debugger/globals.d.ts index 91fa0c951..204a7a701 100644 --- a/packages/debugger/globals.d.ts +++ b/packages/debugger/globals.d.ts @@ -20,7 +20,9 @@ declare global { */ NEXT_PUBLIC_FARCASTER_ATTRIBUTION_FID: string | undefined; - KV_URL: string | undefined; + /** + * Necessary for Farcaster Frames v2 events debugging + */ KV_REST_API_TOKEN: string | undefined; KV_REST_API_URL: string | undefined; } From e723d308a1aba5c8bdc0e427020a15a61e358171 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michal=20Kvasnic=CC=8Ca=CC=81k?= Date: Thu, 19 Dec 2024 13:32:48 +0100 Subject: [PATCH 70/88] chore: integrate farcaster implementation of frames v2 --- .../frame-app-debugger-notifications.tsx | 2 +- .../app/components/frame-app-debugger.tsx | 66 +- .../app/components/frame-debugger.tsx | 29 +- packages/debugger/app/debugger-page.tsx | 3 + .../[namespaceId]/events/route.ts | 8 +- .../app/notifications/[namespaceId]/route.ts | 10 +- packages/debugger/app/notifications/types.ts | 10 +- packages/debugger/package.json | 2 +- packages/frames.js/package.json | 6 +- packages/frames.js/src/farcaster-v2/events.ts | 19 +- packages/frames.js/src/farcaster-v2/types.ts | 169 +--- .../src/frame-parsers/farcasterV2.test.ts | 205 +---- .../src/frame-parsers/farcasterV2.ts | 587 +----------- packages/frames.js/src/frame-parsers/utils.ts | 48 + packages/render/package.json | 37 +- packages/render/src/frame-app/iframe.ts | 138 +++ .../src/frame-app/provider/event-emitter.ts | 97 -- .../render/src/frame-app/provider/helpers.ts | 40 + .../render/src/frame-app/provider/types.ts | 32 - .../render/src/frame-app/provider/wagmi.ts | 243 ++--- packages/render/src/frame-app/types.ts | 85 +- .../src/frame-app/use-resolve-client.ts | 6 +- packages/render/src/frame-app/web-view.ts | 146 +++ packages/render/src/hooks/use-debug-log.ts | 22 + packages/render/src/use-frame-app.ts | 853 +++--------------- yarn.lock | 51 +- 26 files changed, 971 insertions(+), 1943 deletions(-) create mode 100644 packages/render/src/frame-app/iframe.ts delete mode 100644 packages/render/src/frame-app/provider/event-emitter.ts create mode 100644 packages/render/src/frame-app/provider/helpers.ts delete mode 100644 packages/render/src/frame-app/provider/types.ts create mode 100644 packages/render/src/frame-app/web-view.ts create mode 100644 packages/render/src/hooks/use-debug-log.ts diff --git a/packages/debugger/app/components/frame-app-debugger-notifications.tsx b/packages/debugger/app/components/frame-app-debugger-notifications.tsx index 2f48034a6..4a8665129 100644 --- a/packages/debugger/app/components/frame-app-debugger-notifications.tsx +++ b/packages/debugger/app/components/frame-app-debugger-notifications.tsx @@ -131,7 +131,7 @@ export function FrameAppDebuggerNotifications({ ); } - if (!frame.manifest.manifest.frame.webhookUrl) { + if (!frame.manifest.manifest.frame?.webhookUrl) { return ( <> diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 10449e4a5..0296a3ff1 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -3,11 +3,7 @@ import type { FrameLaunchedInContext } from "./frame-debugger"; import { WithTooltip } from "./with-tooltip"; import { Loader2Icon, RefreshCwIcon } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; -import { - type FramePrimaryButton, - type ResolveClientFunction, - useFrameAppInIframe, -} from "@frames.js/render/use-frame-app"; +import { useFrameAppInIframe } from "@frames.js/render/frame-app/iframe"; import { useCallback, useRef, useState } from "react"; import { useWagmiProvider } from "@frames.js/render/frame-app/provider/wagmi"; import { useToast } from "@/components/ui/use-toast"; @@ -22,12 +18,18 @@ import { FrameAppNotificationsManagerProvider, useFrameAppNotificationsManager, } from "../providers/FrameAppNotificationsManagerProvider"; +import { ToastAction } from "@/components/ui/toast"; +import type { + FramePrimaryButton, + ResolveClientFunction, +} from "@frames.js/render/frame-app/types"; type TabValues = "events" | "console" | "notifications"; type FrameAppDebuggerProps = { context: FrameLaunchedInContext; farcasterSigner: FarcasterSignerInstance; + onClose: () => void; }; // in debugger we don't want to automatically reject repeated add frame calls @@ -48,6 +50,7 @@ const addFrameRequestsCache = new (class extends Set { export function FrameAppDebugger({ context, farcasterSigner, + onClose, }: FrameAppDebuggerProps) { const farcasterSignerRef = useRef(farcasterSigner); farcasterSignerRef.current = farcasterSigner; @@ -57,14 +60,16 @@ export function FrameAppDebugger({ }); const { toast } = useToast(); const debuggerConsoleTabRef = useRef(null); - const iframeRef = useRef(null); + const iframeRef = useRef(null); const [activeTab, setActiveTab] = useState("notifications"); const [isAppReady, setIsAppReady] = useState(false); const [primaryButton, setPrimaryButton] = useState<{ button: FramePrimaryButton; callback: () => void; } | null>(null); - const provider = useWagmiProvider(); + const provider = useWagmiProvider({ + debug: true, + }); const resolveClient: ResolveClientFunction = useCallback(async () => { try { const { manager } = await frameAppNotificationManager.promise; @@ -124,7 +129,18 @@ export function FrameAppDebugger({ console.info("sdk.actions.close() called"); toast({ title: "Frame app closed", - description: "The frame app called close() action.", + description: + "The frame app called close() action. Would you like to close it?", + action: ( + { + onClose(); + }} + > + Close + + ), }); }, onOpenUrl(url) { @@ -210,9 +226,6 @@ export function FrameAppDebugger({ throw e; } }, - onDebugEthProviderRequest(parameters) { - console.info("sdk.wallet.ethProvider.request() called", { parameters }); - }, }); return ( @@ -253,20 +266,22 @@ export function FrameAppDebugger({ context.frame.button.action.splashBackgroundColor, }} > -
- {`${name} -
- + {`${name} +
+ +
-
+ )}
))} {frameApp.status === "success" && ( @@ -274,8 +289,11 @@ export function FrameAppDebugger({