diff --git a/.changeset/popular-bats-relate.md b/.changeset/popular-bats-relate.md new file mode 100644 index 00000000..72aaed84 --- /dev/null +++ b/.changeset/popular-bats-relate.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +fix: properly set frame app context on reload diff --git a/packages/debugger/app/components/frame-app-debugger-notifications.tsx b/packages/debugger/app/components/frame-app-debugger-notifications.tsx index 408b517a..90e1e4bc 100644 --- a/packages/debugger/app/components/frame-app-debugger-notifications.tsx +++ b/packages/debugger/app/components/frame-app-debugger-notifications.tsx @@ -19,7 +19,7 @@ import { isValidPartialFrameV2 } from "@frames.js/render/ui/utils"; import type { FarcasterSigner } from "@frames.js/render/identity/farcaster"; type FrameAppDebuggerNotificationsProps = { - frameApp: Extract; + frameApp: UseFrameAppInIframeReturn | null; farcasterSigner: FarcasterSigner | null; }; @@ -27,7 +27,6 @@ export function FrameAppDebuggerNotifications({ frameApp, farcasterSigner, }: FrameAppDebuggerNotificationsProps) { - const frame = frameApp.frame; const frameAppNotificationManager = useFrameAppNotificationsManagerContext(); const [events, setEvents] = useState([]); const notificationsQuery = useQuery({ @@ -102,7 +101,13 @@ export function FrameAppDebuggerNotifications({ } }, [notificationsQuery.data]); - if (!isValidPartialFrameV2(frameApp.frame)) { + if (!frameApp || frameApp.status !== "success") { + return null; + } + + const frame = frameApp.frame; + + if (!isValidPartialFrameV2(frame)) { return ( <> diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 8c2bc7e5..961999f5 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -1,36 +1,22 @@ import "@farcaster/auth-kit/styles.css"; -import { createAppClient, viemConnector, QRCode } from "@farcaster/auth-kit"; import { Button } from "@/components/ui/button"; import type { FrameLaunchedInContext } from "./frame-debugger"; import { WithTooltip } from "./with-tooltip"; -import { Loader2Icon, RefreshCwIcon } from "lucide-react"; +import { RefreshCwIcon } from "lucide-react"; import { Card, CardContent } from "@/components/ui/card"; -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"; +import { type UseFrameAppInIframeReturn } from "@frames.js/render/frame-app/iframe"; +import { useReducer, useRef, useState } from "react"; 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 type { FarcasterSignerInstance } from "@frames.js/render/identity/farcaster"; import { FrameAppDebuggerNotifications } from "./frame-app-debugger-notifications"; import { FrameAppNotificationsManagerProvider, useFrameAppNotificationsManager, } from "../providers/FrameAppNotificationsManagerProvider"; -import { ToastAction } from "@/components/ui/toast"; -import type { - FramePrimaryButton, - ResolveClientFunction, -} from "@frames.js/render/frame-app/types"; -import { useConfig } from "wagmi"; -import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; -import { z } from "zod"; -import { Dialog, DialogContent } from "@/components/ui/dialog"; -import { useCopyToClipboard } from "../hooks/useCopyToClipboad"; import { FrameAppDebuggerViewProfileDialog } from "./frame-app-debugger-view-profile-dialog"; +import { FrameApp } from "./frame-app"; type TabValues = "events" | "console" | "notifications"; @@ -40,36 +26,15 @@ type FrameAppDebuggerProps = { onClose: () => void; }; -// 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; - } -})(); - -const appClient = createAppClient({ - ethereum: viemConnector(), -}); - export function FrameAppDebugger({ context, farcasterSigner, onClose, }: FrameAppDebuggerProps) { - const copyFarcasterSignInLink = useCopyToClipboard(); - const [ - farcasterSignInAbortControllerAndURL, - setFarcasterSignInAbortControllerURL, - ] = useState<{ controller: AbortController; url: URL } | null>(null); - const config = useConfig(); + const [appIdCounter, reloadApp] = useReducer((state) => state + 1, 0); + const [frameApp, setFrameApp] = useState( + null + ); const farcasterSignerRef = useRef(farcasterSigner); farcasterSignerRef.current = farcasterSigner; @@ -89,338 +54,12 @@ export function FrameAppDebugger({ farcasterSigner, context, }); - const { toast } = useToast(); const debuggerConsoleTabRef = 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({ - debug: true, - }); - /** - * we have to store promise in ref otherwise it will always invalidate the frame app hooks - * which happens for example when you disable notifications from notifications panel - */ - const frameAppNotificationManagerPromiseRef = useRef( - frameAppNotificationManager.promise - ); - const resolveClient: ResolveClientFunction = useCallback(async () => { - try { - const clientInfoResponse = await fetch("/client-info"); - - if (!clientInfoResponse.ok) { - throw new Error("Failed to fetch client info"); - } - - const parseClientInfo = z.object({ - fid: z.number().int(), - }); - - const clientInfo = parseClientInfo.parse(await clientInfoResponse.json()); - - const { manager } = await frameAppNotificationManagerPromiseRef.current; - const clientFid = clientInfo.fid; - - if (!manager.state || manager.state.frame.status === "removed") { - return { - clientFid, - added: false, - }; - } - - return { - clientFid, - added: true, - notificationDetails: - manager.state.frame.notificationDetails ?? undefined, - }; - } catch (e) { - console.error(e); - - toast({ - title: "Unexpected error", - description: - "Failed to load notifications settings. Check the console for more details.", - variant: "destructive", - }); - - return { - clientFid: -1, - added: false, - }; - } - }, [toast]); const [viewFidProfile, setViewFidProfile] = useState(null); - const frameApp = useFrameAppInIframe({ - debug: true, - source: context.parseResult, - client: resolveClient, - location: - context.context === "button_press" - ? { - type: "launcher", - } - : { - type: "cast_embed", - embed: "", - cast: fallbackFrameContext.castId, - }, - user: userContext.current, - provider, - proxyUrl: "/frames", - addFrameRequestsCache, - onReady(options) { - console.info("sdk.actions.ready() called", { options }); - setIsAppReady(true); - }, - onClose() { - console.info("sdk.actions.close() called"); - toast({ - title: "Frame app closed", - description: - "The frame app called close() action. Would you like to close it?", - action: ( - { - onClose(); - }} - > - Close - - ), - }); - }, - onOpenUrl(url) { - console.info("sdk.actions.openUrl() called", { url }); - window.open(url, "_blank"); - }, - onPrimaryButtonSet(button, buttonCallback) { - console.info("sdk.actions.setPrimaryButton() called", { button }); - setPrimaryButton({ - button, - callback: () => { - console.info("primary button clicked"); - buttonCallback(); - }, - }); - }, - async onAddFrameRequested(parseResult) { - console.info("sdk.actions.addFrame() called"); - - if (frameAppNotificationManager.status === "pending") { - toast({ - title: "Notifications manager not ready", - description: - "Notifications manager is not ready. Please wait a moment.", - variant: "destructive", - }); - - 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; - - 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; - } - - try { - const result = - await frameAppNotificationManager.data.manager.addFrame(); - - return { - added: true, - notificationDetails: result, - }; - } catch (e) { - console.error(e); - - toast({ - title: "Failed to add frame", - description: - "Failed to add frame to the notifications manager. Check the console for more details.", - variant: "destructive", - }); - - throw e; - } - }, - onEIP6963RequestProviderRequested({ endpoint }) { - if (!config._internal.mipd) { - return; - } - - config._internal.mipd.getProviders().map((providerInfo) => { - endpoint.emit({ - event: "eip6963:announceProvider", - info: providerInfo.info as EIP6963ProviderInfo, - }); - }); - }, - async onSignIn({ nonce, notBefore, expirationTime, frame }) { - console.info("sdk.actions.signIn() called", { - nonce, - notBefore, - expirationTime, - }); - let abortTimeout: NodeJS.Timeout | undefined; - - try { - const frameUrl = frame.frame.button?.action?.url; - - if (!frameUrl) { - throw new Error("Frame is malformed, action url is missing"); - } - - const createChannelResult = await appClient.createChannel({ - nonce, - notBefore, - expirationTime, - siweUri: frameUrl, - domain: new URL(frameUrl).hostname, - }); - - if (createChannelResult.isError) { - throw ( - createChannelResult.error || - new Error("Failed to create sign in channel") - ); - } - - const abortController = new AbortController(); - - setFarcasterSignInAbortControllerURL({ - controller: abortController, - url: new URL(createChannelResult.data.url), - }); - - const signInTimeoutReason = "Sign in timed out"; - - // abort controller after 30 seconds - abortTimeout = setTimeout(() => { - abortController.abort(signInTimeoutReason); - }, 30000); - - let status: Awaited>; - - while (true) { - if (abortController.signal.aborted) { - if (abortTimeout) { - clearTimeout(abortTimeout); - } - - if (abortController.signal.reason === signInTimeoutReason) { - toast({ - title: "Sign in timed out", - variant: "destructive", - }); - } - - throw new Error(abortController.signal.reason); - } - - status = await appClient.status({ - channelToken: createChannelResult.data.channelToken, - }); - - if (!status.isError && status.data.state === "completed") { - break; - } - - await new Promise((r) => setTimeout(r, 1000)); - } - - clearTimeout(abortTimeout); - - const { message, signature } = status.data; - - if (!(signature && message)) { - throw new Error("Signature or message is missing"); - } - - return { - signature, - message, - }; - } finally { - clearTimeout(abortTimeout); - setFarcasterSignInAbortControllerURL(null); - } - }, - async onViewProfile(params) { - console.info("sdk.actions.viewProfile() called", params); - setViewFidProfile(params.fid); - }, - }); return ( <> - {!!farcasterSignInAbortControllerAndURL && ( - { - farcasterSignInAbortControllerAndURL.controller.abort( - "User closed sign in dialog" - ); - }} - > - -
-

Sign in with Farcaster

- - or - -
-
-
- )}
@@ -429,12 +68,7 @@ export function FrameAppDebugger({ className="flex flex-row gap-3 items-center shadow-sm border" variant={"outline"} onClick={() => { - // reload iframe - if (iframeRef.current) { - iframeRef.current.src = ""; - iframeRef.current.src = context.frame.button.action.url; - setIsAppReady(false); - } + reloadApp(); }} > @@ -443,78 +77,18 @@ export function FrameAppDebugger({
-
- {frameApp.status === "pending" || - (!isAppReady && ( -
- {context.frame.button.action.splashImageUrl && ( -
- {`${name} -
- -
-
- )} -
- ))} - {frameApp.status === "success" && ( - <> -