diff --git a/packages/debugger/app/components/action-debugger.tsx b/packages/debugger/app/components/action-debugger.tsx index c6835584b..851358855 100644 --- a/packages/debugger/app/components/action-debugger.tsx +++ b/packages/debugger/app/components/action-debugger.tsx @@ -9,7 +9,6 @@ import { cn } from "@/lib/utils"; import { type FarcasterFrameContext, type FrameActionBodyPayload, - OnComposeFormActionFuncReturnType, defaultTheme, } from "@frames.js/render"; import { ParsingReport } from "frames.js"; @@ -26,7 +25,6 @@ import React, { useEffect, useImperativeHandle, useMemo, - useRef, useState, } from "react"; import { Button } from "../../@/components/ui/button"; @@ -37,12 +35,9 @@ import { useFrame } from "@frames.js/render/use-frame"; import { WithTooltip } from "./with-tooltip"; import { useToast } from "@/components/ui/use-toast"; import type { CastActionDefinitionResponse } from "../frames/route"; -import { ComposerFormActionDialog } from "./composer-form-action-dialog"; -import { AwaitableController } from "../lib/awaitable-controller"; -import type { ComposerActionFormResponse } from "frames.js/types"; -import { CastComposer, CastComposerRef } from "./cast-composer"; 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; @@ -227,47 +222,9 @@ export const ActionDebugger = React.forwardRef< } }, [copySuccess, setCopySuccess]); - const [composeFormActionDialogSignal, setComposerFormActionDialogSignal] = - useState | null>(null); const actionFrameState = useFrame({ ...farcasterFrameConfig, - async onComposerFormAction({ form }) { - try { - const dialogSignal = new AwaitableController< - OnComposeFormActionFuncReturnType, - ComposerActionFormResponse - >(form); - - setComposerFormActionDialogSignal(dialogSignal); - - const result = await dialogSignal; - - // if result is undefined then user closed the dialog window without submitting - // otherwise we have updated data - if (result?.composerActionState) { - castComposerRef.current?.updateState(result.composerActionState); - } - - return result; - } catch (e) { - console.error(e); - toast({ - title: "Error occurred", - description: - e instanceof Error - ? e.message - : "Unexpected error, check the console for more info", - variant: "destructive", - }); - } finally { - setComposerFormActionDialogSignal(null); - } - }, }); - const castComposerRef = useRef(null); const [castActionDefinition, setCastActionDefinition] = useState refreshUrl()} > - { - if (actionMetadataItem.status !== "success") { - console.error(actionMetadataItem); - - toast({ - title: "Invalid action metadata", - description: - "Please check the console for more information", - variant: "destructive", - }); - return; - } - - Promise.resolve( - actionFrameState.onComposerActionButtonPress({ - castAction: { - ...actionMetadataItem.action, - url: actionMetadataItem.url, - }, - composerActionState, - // clear stack, this removes first item that will appear in the debugger - clearStack: true, - }) - ).catch((e: unknown) => { - // eslint-disable-next-line no-console -- provide feedback to the user - console.error(e); - }); + { + setActiveTab("cast-action"); }} /> - - {!!composeFormActionDialogSignal && ( - { - composeFormActionDialogSignal.resolve(undefined); - }} - onSave={({ composerState }) => { - composeFormActionDialogSignal.resolve({ - composerActionState: composerState, - }); - }} - onTransaction={farcasterFrameConfig.onTransaction} - onSignature={farcasterFrameConfig.onSignature} - /> - )} diff --git a/packages/debugger/app/components/cast-composer.tsx b/packages/debugger/app/components/cast-composer.tsx index 8452fa809..e353259f1 100644 --- a/packages/debugger/app/components/cast-composer.tsx +++ b/packages/debugger/app/components/cast-composer.tsx @@ -11,29 +11,21 @@ import { ExternalLinkIcon, } from "lucide-react"; import IconByName from "./octicons"; -import { useFrame } from "@frames.js/render/use-frame"; +import { useFrame_unstable } from "@frames.js/render/use-frame"; import { WithTooltip } from "./with-tooltip"; -import type { - FarcasterFrameContext, - FrameActionBodyPayload, - FrameStackDone, -} from "@frames.js/render"; +import { fallbackFrameContext } from "@frames.js/render"; import { FrameUI } from "./frame-ui"; import { useToast } from "@/components/ui/use-toast"; import { ToastAction } from "@radix-ui/react-toast"; import Link from "next/link"; -import type { FarcasterSigner } from "@frames.js/render/identity/farcaster"; +import { useDebuggerFrameState } from "@frames.js/render/unstable-use-debugger-frame-state"; +import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; +import { useAccount } from "wagmi"; +import { FrameStackDone } from "@frames.js/render/unstable-types"; type CastComposerProps = { composerAction: Partial; onComposerActionClick: (state: ComposerActionState) => any; - farcasterFrameConfig: Parameters< - typeof useFrame< - FarcasterSigner | null, - FrameActionBodyPayload, - FarcasterFrameContext - > - >[0]; }; export type CastComposerRef = { @@ -43,7 +35,7 @@ export type CastComposerRef = { export const CastComposer = React.forwardRef< CastComposerRef, CastComposerProps ->(({ composerAction, farcasterFrameConfig, onComposerActionClick }, ref) => { +>(({ composerAction, onComposerActionClick }, ref) => { const [state, setState] = useState({ text: "", embeds: [], @@ -79,7 +71,6 @@ export const CastComposer = React.forwardRef< {state.embeds.slice(0, 2).map((embed, index) => (
  • { const filteredEmbeds = state.embeds.filter( (_, i) => i !== index @@ -119,13 +110,6 @@ export const CastComposer = React.forwardRef< CastComposer.displayName = "CastComposer"; type CastEmbedPreviewProps = { - farcasterFrameConfig: Parameters< - typeof useFrame< - FarcasterSigner | null, - FrameActionBodyPayload, - FarcasterFrameContext - > - >[0]; url: string; onRemove: () => void; }; @@ -147,15 +131,19 @@ function isAtLeastPartialFrame(stackItem: FrameStackDone): boolean { ); } -function CastEmbedPreview({ - farcasterFrameConfig, - onRemove, - url, -}: CastEmbedPreviewProps) { +function CastEmbedPreview({ onRemove, url }: CastEmbedPreviewProps) { + const account = useAccount(); const { toast } = useToast(); - const frame = useFrame({ - ...farcasterFrameConfig, + const farcasterIdentity = useFarcasterIdentity(); + const frame = useFrame_unstable({ + frameStateHook: useDebuggerFrameState, + connectedAddress: account.address, homeframeUrl: url, + frameActionProxy: "/frames", + frameGetProxy: "/frames", + resolveSigner() { + return farcasterIdentity.withContext(fallbackFrameContext); + }, }); const handleFrameError = useCallback( diff --git a/packages/debugger/app/components/composer-action-debugger.tsx b/packages/debugger/app/components/composer-action-debugger.tsx new file mode 100644 index 000000000..faa620999 --- /dev/null +++ b/packages/debugger/app/components/composer-action-debugger.tsx @@ -0,0 +1,51 @@ +import type { + ComposerActionResponse, + ComposerActionState, +} from "frames.js/types"; +import { CastComposer, CastComposerRef } from "./cast-composer"; +import { useRef, useState } from "react"; +import { ComposerFormActionDialog } from "./composer-form-action-dialog"; +import { useFarcasterIdentity } from "../hooks/useFarcasterIdentity"; + +type ComposerActionDebuggerProps = { + url: string; + actionMetadata: Partial; + onToggleToCastActionDebugger: () => void; +}; + +export function ComposerActionDebugger({ + actionMetadata, + url, + onToggleToCastActionDebugger, +}: ComposerActionDebuggerProps) { + const castComposerRef = useRef(null); + const signer = useFarcasterIdentity(); + const [actionState, setActionState] = useState( + null + ); + + return ( + <> + + {!!actionState && ( + { + setActionState(null); + }} + onSubmit={(newActionState) => { + castComposerRef.current?.updateState(newActionState); + setActionState(null); + }} + onToggleToCastActionDebugger={onToggleToCastActionDebugger} + /> + )} + + ); +} diff --git a/packages/debugger/app/components/composer-form-action-dialog.tsx b/packages/debugger/app/components/composer-form-action-dialog.tsx index abea404db..11f98d54a 100644 --- a/packages/debugger/app/components/composer-form-action-dialog.tsx +++ b/packages/debugger/app/components/composer-form-action-dialog.tsx @@ -5,221 +5,158 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { OnSignatureFunc, OnTransactionFunc } from "@frames.js/render"; -import type { - ComposerActionFormResponse, - ComposerActionState, -} from "frames.js/types"; -import { useCallback, useEffect, useRef } from "react"; -import { Abi, TypedDataDomain } from "viem"; -import { z } from "zod"; - -const createCastRequestSchemaLegacy = z.object({ - type: z.literal("createCast"), - data: z.object({ - cast: z.object({ - parent: z.string().optional(), - text: z.string(), - embeds: z.array(z.string().min(1).url()).min(1), - }), - }), -}); - -const ethSendTransactionActionSchema = z.object({ - chainId: z.string(), - method: z.literal("eth_sendTransaction"), - attribution: z.boolean().optional(), - params: z.object({ - abi: z.custom(), - to: z.custom<`0x${string}`>( - (val): val is `0x${string}` => - typeof val === "string" && val.startsWith("0x") - ), - value: z.string().optional(), - data: z - .custom<`0x${string}`>( - (val): val is `0x${string}` => - typeof val === "string" && val.startsWith("0x") - ) - .optional(), - }), -}); - -const ethSignTypedDataV4ActionSchema = z.object({ - chainId: z.string(), - method: z.literal("eth_signTypedData_v4"), - params: z.object({ - domain: z.custom(), - types: z.unknown(), - primaryType: z.string(), - message: z.record(z.unknown()), - }), -}); - -const walletActionRequestSchema = z.object({ - jsonrpc: z.literal("2.0"), - id: z.string(), - method: z.literal("fc_requestWalletAction"), - params: z.object({ - action: z.union([ - ethSendTransactionActionSchema, - ethSignTypedDataV4ActionSchema, - ]), - }), -}); - -const createCastRequestSchema = z.object({ - jsonrpc: z.literal("2.0"), - id: z.union([z.string(), z.number(), z.null()]), - method: z.literal("fc_createCast"), - params: z.object({ - cast: z.object({ - parent: z.string().optional(), - text: z.string(), - embeds: z.array(z.string().min(1).url()).min(1), - }), - }), -}); - -const composerActionMessageSchema = z.union([ - createCastRequestSchemaLegacy, - walletActionRequestSchema, - createCastRequestSchema, -]); +import { useComposerAction } from "@frames.js/render/use-composer-action"; +import type { ComposerActionState } from "frames.js/types"; +import { useRef } from "react"; +import { + useAccount, + useChainId, + useSendTransaction, + useSignTypedData, + useSwitchChain, +} from "wagmi"; +import { useConnectModal } from "@rainbow-me/rainbowkit"; +import { useToast } from "@/components/ui/use-toast"; +import { ToastAction } from "@/components/ui/toast"; +import { parseEther } from "viem"; +import { parseChainId } from "../lib/utils"; +import { FarcasterMultiSignerInstance } from "@frames.js/render/identity/farcaster"; +import { AlertTriangleIcon, Loader2Icon } from "lucide-react"; type ComposerFormActionDialogProps = { - composerActionForm: ComposerActionFormResponse; + actionState: ComposerActionState; + url: string; + signer: FarcasterMultiSignerInstance; onClose: () => void; - onSave: (arg: { composerState: ComposerActionState }) => void; - onTransaction?: OnTransactionFunc; - onSignature?: OnSignatureFunc; - // TODO: Consider moving this into return value of onTransaction - connectedAddress?: `0x${string}`; + onSubmit: (actionState: ComposerActionState) => void; + onToggleToCastActionDebugger: () => void; }; -export function ComposerFormActionDialog({ - composerActionForm, +export const ComposerFormActionDialog = ({ + actionState, + signer, + url, onClose, - onSave, - onTransaction, - onSignature, - connectedAddress, -}: ComposerFormActionDialogProps) { - const onSaveRef = useRef(onSave); - onSaveRef.current = onSave; - + onSubmit, + onToggleToCastActionDebugger, +}: ComposerFormActionDialogProps) => { const iframeRef = useRef(null); + const { toast } = useToast(); + const account = useAccount(); + const currentChainId = useChainId(); + const { switchChainAsync } = useSwitchChain(); + const { sendTransactionAsync } = useSendTransaction(); + const { signTypedDataAsync } = useSignTypedData(); + const { openConnectModal } = useConnectModal(); + + const result = useComposerAction({ + url, + enabled: !signer.isLoadingSigner, + proxyUrl: "/frames", + actionState, + signer, + async resolveAddress() { + if (account.address) { + return account.address; + } - const postMessageToIframe = useCallback( - (message: any) => { - if (iframeRef.current && iframeRef.current.contentWindow) { - iframeRef.current.contentWindow.postMessage( - message, - new URL(composerActionForm.url).origin - ); + if (!openConnectModal) { + throw new Error("Connect modal is not available"); } + + openConnectModal(); + + return null; }, - [composerActionForm.url] - ); + onError(error) { + console.error(error); + + 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: ( + { + onToggleToCastActionDebugger(); + }} + > + Switch + + ), + }); - useEffect(() => { - const handleMessage = (event: MessageEvent) => { - if (event.origin !== new URL(composerActionForm.url).origin) { return; } - const result = composerActionMessageSchema.safeParse(event.data); + toast({ + title: "Error occurred", + description: ( +
    +

    {error.message}

    +

    Please check the console for more information

    +
    + ), + variant: "destructive", + }); + }, + async onCreateCast(arg) { + onSubmit(arg.cast); + }, + async onSignature({ action, address }) { + const { chainId, params } = action; + const requestedChainId = parseChainId(chainId); - // on error is not called here because there can be different messages that don't have anything to do with composer form actions - // instead we are just waiting for the correct message - if (!result.success) { - console.warn("Invalid message received", event.data, result.error); - return; + if (currentChainId !== requestedChainId) { + await switchChainAsync({ chainId: requestedChainId }); } - const message = result.data; + const hash = await signTypedDataAsync(params); - if ("type" in message) { - // Handle legacy messages - onSaveRef.current({ - composerState: message.data.cast, - }); - } else if (message.method === "fc_requestWalletAction") { - if (message.params.action.method === "eth_sendTransaction") { - onTransaction?.({ - transactionData: message.params.action, - }).then((txHash) => { - if (txHash) { - postMessageToIframe({ - jsonrpc: "2.0", - id: message.id, - result: { - address: connectedAddress, - transactionId: txHash, - }, - }); - } else { - postMessageToIframe({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32000, - message: "User rejected the request", - }, - }); - } - }); - } else if (message.params.action.method === "eth_signTypedData_v4") { - onSignature?.({ - signatureData: { - chainId: message.params.action.chainId, - method: message.params.action.method, - params: { - domain: message.params.action.params.domain, - types: message.params.action.params.types as any, - primaryType: message.params.action.params.primaryType, - message: message.params.action.params.message, - }, - }, - }).then((signature) => { - if (signature) { - postMessageToIframe({ - jsonrpc: "2.0", - id: message.id, - result: { - address: connectedAddress, - transactionId: signature, - }, - }); - } else { - postMessageToIframe({ - jsonrpc: "2.0", - id: message.id, - error: { - code: -32000, - message: "User rejected the request", - }, - }); - } - }); - } - } else if (message.method === "fc_createCast") { - if (message.params.cast.embeds.length > 2) { - console.warn("Only first 2 embeds are shown in the cast"); - } + return { + address, + hash, + }; + }, + async onTransaction({ action, address }) { + const { chainId, params } = action; + const requestedChainId = parseChainId(chainId); - onSaveRef.current({ - composerState: message.params.cast, - }); + if (currentChainId !== requestedChainId) { + await switchChainAsync({ chainId: requestedChainId }); } - }; - window.addEventListener("message", handleMessage); + const hash = await sendTransactionAsync({ + to: params.to, + data: params.data, + value: parseEther(params.value ?? "0"), + }); + + return { + address, + hash, + }; + }, + onPostResponseToTarget(message, form) { + if (iframeRef.current && iframeRef.current.contentWindow) { + iframeRef.current.contentWindow.postMessage( + message, + new URL(form.url).origin + ); + } + }, + }); - return () => { - window.removeEventListener("message", handleMessage); - }; - }, []); + if (result.status === "idle") { + return null; + } return ( - - {composerActionForm.title} - -
    - -
    - - - {new URL(composerActionForm.url).hostname} - - + {result.status === "loading" && ( + <> +
    + +
    + + )} + {result.status === "error" && ( + <> +
    + +

    + Something went wrong +

    +

    Check the console

    +
    + + )} + {result.status === "success" && ( + <> + + {result.data.title} + +
    + +
    + + + {new URL(result.data.url).hostname} + + + + )}
    ); -} +}; + +ComposerFormActionDialog.displayName = "ComposerFormActionDialog"; diff --git a/packages/debugger/app/debugger-page.tsx b/packages/debugger/app/debugger-page.tsx index afba6a646..cd390136b 100644 --- a/packages/debugger/app/debugger-page.tsx +++ b/packages/debugger/app/debugger-page.tsx @@ -56,7 +56,6 @@ import type { import { useAnonymousIdentity } from "@frames.js/render/identity/anonymous"; import { useFarcasterFrameContext, - useFarcasterMultiIdentity, type FarcasterSigner, } from "@frames.js/render/identity/farcaster"; import { @@ -67,25 +66,14 @@ import { useXmtpFrameContext, useXmtpIdentity, } from "@frames.js/render/identity/xmtp"; +import { useFarcasterIdentity } from "./hooks/useFarcasterIdentity"; +import { InvalidChainIdError, parseChainId } from "./lib/utils"; const FALLBACK_URL = process.env.NEXT_PUBLIC_DEBUGGER_DEFAULT_URL || "http://localhost:3000"; -class InvalidChainIdError extends Error {} class CouldNotChangeChainError extends Error {} -function isValidChainId(id: string): boolean { - return id.startsWith("eip155:"); -} - -function parseChainId(id: string): number { - if (!isValidChainId(id)) { - throw new InvalidChainIdError(`Invalid chainId ${id}`); - } - - return parseInt(id.split("eip155:")[1]!); -} - const anonymousFrameContext = {}; export default function DebuggerPage({ @@ -257,26 +245,8 @@ export default function DebuggerPage({ refreshUrl(url); }, [url, protocolConfiguration, refreshUrl, toast, debuggerConsole]); - const farcasterSignerState = useFarcasterMultiIdentity({ - onMissingIdentity() { - toast({ - title: "Please select an identity", - description: - "In order to test the buttons you need to select an identity first", - variant: "destructive", - action: ( - { - selectProtocolButtonRef.current?.click(); - }} - type="button" - > - Select identity - - ), - }); - }, + const farcasterSignerState = useFarcasterIdentity({ + selectProtocolButtonRef, }); const xmtpSignerState = useXmtpIdentity(); const lensSignerState = useLensIdentity(); @@ -301,8 +271,6 @@ export default function DebuggerPage({ }, }); - const anonymousFrameContext = {}; - const onConnectWallet: OnConnectWalletFunc = useCallback(async () => { if (!openConnectModal) { throw new Error(`openConnectModal not implemented`); @@ -640,7 +608,6 @@ export default function DebuggerPage({ }; }, [ anonymousSignerState, - anonymousFrameContext, farcasterFrameConfig, lensFrameContext.frameContext, lensSignerState, diff --git a/packages/debugger/app/frames/route.ts b/packages/debugger/app/frames/route.ts index c486a7551..b6f987512 100644 --- a/packages/debugger/app/frames/route.ts +++ b/packages/debugger/app/frames/route.ts @@ -1,51 +1,11 @@ -import { type FrameActionPayload } from "frames.js"; +import { POST as handlePOSTRequest } from "@frames.js/render/next"; import { type NextRequest } from "next/server"; import { getAction } from "../actions/getAction"; import { persistMockResponsesForDebugHubRequests } from "../utils/mock-hub-utils"; import type { SupportedParsingSpecification } from "frames.js"; import { parseFramesWithReports } from "frames.js/parseFramesWithReports"; -import { z } from "zod"; import type { ParseActionResult } from "../actions/types"; import type { ParseFramesWithReportsResult } from "frames.js/frame-parsers"; -import type { JsonObject } from "frames.js/types"; - -const castActionMessageParser = z.object({ - type: z.literal("message"), - message: z.string().min(1), -}); - -const castActionFrameParser = z.object({ - type: z.literal("frame"), - frameUrl: z.string().min(1).url(), -}); - -const composerActionFormParser = z.object({ - type: z.literal("form"), - url: z.string().min(1).url(), - title: z.string().min(1), -}); - -const jsonResponseParser = z.preprocess( - (data) => { - if (typeof data === "object" && data !== null && !("type" in data)) { - return { - type: "message", - ...data, - }; - } - - return data; - }, - z.discriminatedUnion("type", [ - castActionFrameParser, - castActionMessageParser, - composerActionFormParser, - ]) -); - -const errorResponseParser = z.object({ - message: z.string().min(1), -}); export type CastActionDefinitionResponse = ParseActionResult & { type: "action"; @@ -126,129 +86,7 @@ export async function GET(request: NextRequest): Promise { /** Proxies frame actions to avoid CORS issues and preserve user IP privacy */ export async function POST(req: NextRequest): Promise { - const body = (await req.clone().json()) as FrameActionPayload; - const isPostRedirect = - req.nextUrl.searchParams.get("postType") === "post_redirect"; - const isTransactionRequest = - req.nextUrl.searchParams.get("postType") === "tx"; - const postUrl = req.nextUrl.searchParams.get("postUrl"); - const specification = - req.nextUrl.searchParams.get("specification") ?? "farcaster"; - - if (!isSpecificationValid(specification)) { - return Response.json({ message: "Invalid specification" }, { status: 400 }); - } - - // TODO: refactor useful logic back into render package - - if (specification === "farcaster") { - await persistMockResponsesForDebugHubRequests(req); - } + await persistMockResponsesForDebugHubRequests(req); - if (!postUrl) { - return Response.json({ message: "Invalid post URL" }, { status: 400 }); - } - - try { - const r = await fetch(postUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - redirect: isPostRedirect ? "manual" : undefined, - body: JSON.stringify(body), - }); - - if (r.status === 302) { - return Response.json( - { - location: r.headers.get("location"), - }, - { status: 302 } - ); - } - - // this is an error, just return response as is - if (r.status >= 500) { - return Response.json(await r.text(), { status: r.status }); - } - - if (r.status >= 400 && r.status < 500) { - const parseResult = await z - .promise(errorResponseParser) - .safeParseAsync(r.clone().json()); - - if (!parseResult.success) { - return Response.json( - { message: await r.clone().text() }, - { status: r.status } - ); - } - - const headers = new Headers(r.headers); - // Proxied requests could have content-encoding set, which breaks the response - headers.delete("content-encoding"); - return new Response(r.body, { - headers, - status: r.status, - statusText: r.statusText, - }); - } - - if (isPostRedirect && r.status !== 302) { - return Response.json( - { - message: `Invalid response status code for post redirect button, 302 expected, got ${r.status}`, - }, - { status: 400 } - ); - } - - if (isTransactionRequest) { - const transaction = (await r.json()) as JsonObject; - return Response.json(transaction); - } - - // Content type is JSON, could be an action - if (r.headers.get("content-type")?.includes("application/json")) { - const parseResult = await z - .promise(jsonResponseParser) - .safeParseAsync(r.clone().json()); - - if (!parseResult.success) { - throw new Error("Invalid frame response"); - } - - const headers = new Headers(r.headers); - // Proxied requests could have content-encoding set, which breaks the response - headers.delete("content-encoding"); - return new Response(r.body, { - headers, - status: r.status, - statusText: r.statusText, - }); - } - - const html = await r.text(); - - const parseResult = parseFramesWithReports({ - html, - fallbackPostUrl: body.untrustedData.url, - fromRequestMethod: "POST", - }); - - return Response.json({ - type: "frame", - ...parseResult, - } satisfies FrameDefinitionResponse); - } catch (err) { - // eslint-disable-next-line no-console -- provide feedback to the user - console.error(err); - return Response.json( - { - message: String(err), - }, - { status: 500 } - ); - } + return handlePOSTRequest(req); } diff --git a/packages/debugger/app/hooks/useFarcasterIdentity.tsx b/packages/debugger/app/hooks/useFarcasterIdentity.tsx new file mode 100644 index 000000000..b3a1f544b --- /dev/null +++ b/packages/debugger/app/hooks/useFarcasterIdentity.tsx @@ -0,0 +1,44 @@ +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"; + +const sharedStorage = new WebStorage(); + +type Options = Omit< + Parameters[0], + "onMissingIdentity" +> & { + selectProtocolButtonRef?: React.RefObject; +}; + +export function useFarcasterIdentity({ + selectProtocolButtonRef, + ...options +}: Options = {}) { + const { toast } = useToast(); + + return useFarcasterMultiIdentity({ + ...(options ?? {}), + storage: sharedStorage, + onMissingIdentity() { + toast({ + title: "Please select an identity", + description: + "In order to test the buttons you need to select an identity first", + variant: "destructive", + action: selectProtocolButtonRef?.current ? ( + { + selectProtocolButtonRef?.current?.click(); + }} + type="button" + > + Select identity + + ) : undefined, + }); + }, + }); +} diff --git a/packages/debugger/app/lib/utils.ts b/packages/debugger/app/lib/utils.ts index 0c58f89ad..7d5402eff 100644 --- a/packages/debugger/app/lib/utils.ts +++ b/packages/debugger/app/lib/utils.ts @@ -18,3 +18,17 @@ export function hasWarnings(reports: Record): boolean { report.some((r) => r.level === "warning") ); } + +export class InvalidChainIdError extends Error {} + +export function isValidChainId(id: string): boolean { + return id.startsWith("eip155:"); +} + +export function parseChainId(id: string): number { + if (!isValidChainId(id)) { + throw new InvalidChainIdError(`Invalid chainId ${id}`); + } + + return parseInt(id.split("eip155:")[1]!); +} diff --git a/packages/debugger/app/utils/mock-hub-utils.ts b/packages/debugger/app/utils/mock-hub-utils.ts index 62001f16c..f57c0fab0 100644 --- a/packages/debugger/app/utils/mock-hub-utils.ts +++ b/packages/debugger/app/utils/mock-hub-utils.ts @@ -71,14 +71,17 @@ export async function loadMockResponseForDebugHubRequest( } export async function persistMockResponsesForDebugHubRequests(req: Request) { - const { - mockData, - untrustedData: { fid: requesterFid, castId }, - } = (await req.clone().json()) as { - mockData: MockHubActionContext; - untrustedData: { fid: string; castId: { fid: string; hash: string } }; + const { mockData, untrustedData } = (await req.clone().json()) as { + mockData?: MockHubActionContext; + untrustedData?: { fid: string; castId?: { fid: string; hash: string } }; }; + if (!mockData || !untrustedData?.castId) { + return; + } + + const { fid: requesterFid, castId } = untrustedData; + const requesterFollowsCaster = `/v1/linkById?${sortedSearchParamsString( new URLSearchParams({ fid: requesterFid,