"use client"; import { useEffect, useCallback, useState, useMemo } from "react"; import { signIn, signOut, getCsrfToken } from "next-auth/react"; import sdk, { FrameNotificationDetails, type FrameContext, } from "@farcaster/frame-sdk"; import { useAccount, useSendTransaction, useSignMessage, useSignTypedData, useWaitForTransactionReceipt, useDisconnect, useConnect, useSwitchChain, useChainId, } from "wagmi"; import { config } from "~/components/providers/WagmiProvider"; import { Button } from "~/components/ui/Button"; import { truncateAddress } from "~/lib/truncateAddress"; import { base, optimism } from "wagmi/chains"; import { BaseError, UserRejectedRequestError } from "viem"; import { useSession } from "next-auth/react" import { SignInResult } from "@farcaster/frame-core/dist/actions/signIn"; export default function Demo( { title }: { title?: string } = { title: "Frames v2 Demo" } ) { const [isSDKLoaded, setIsSDKLoaded] = useState(false); const [context, setContext] = useState(); const [isContextOpen, setIsContextOpen] = useState(false); const [txHash, setTxHash] = useState(null); const [addFrameResult, setAddFrameResult] = useState(""); const [notificationDetails, setNotificationDetails] = useState(null); const [sendNotificationResult, setSendNotificationResult] = useState(""); useEffect(() => { setNotificationDetails(context?.client.notificationDetails ?? null); }, [context]); const { address, isConnected } = useAccount(); const chainId = useChainId(); const { sendTransaction, error: sendTxError, isError: isSendTxError, isPending: isSendTxPending, } = useSendTransaction(); const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: txHash as `0x${string}`, }); const { signTypedData, error: signTypedError, isError: isSignTypedError, isPending: isSignTypedPending, } = useSignTypedData(); const { disconnect } = useDisconnect(); const { connect } = useConnect(); const { switchChain, error: switchChainError, isError: isSwitchChainError, isPending: isSwitchChainPending, } = useSwitchChain(); const handleSwitchChain = useCallback(() => { switchChain({ chainId: chainId === base.id ? optimism.id : base.id }); }, [switchChain, chainId]); 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 openWarpcastUrl = useCallback(() => { sdk.actions.openUrl("https://warpcast.com/~/compose"); }, []); const close = useCallback(() => { sdk.actions.close(); }, []); const addFrame = useCallback(async () => { try { setNotificationDetails(null); const result = await sdk.actions.addFrame(); if (result.added) { if (result.notificationDetails) { setNotificationDetails(result.notificationDetails); } setAddFrameResult( result.notificationDetails ? `Added, got notificaton token ${result.notificationDetails.token} and url ${result.notificationDetails.url}` : "Added, got no notification details" ); } else { setAddFrameResult(`Not added: ${result.reason}`); } } catch (error) { setAddFrameResult(`Error: ${error}`); } }, []); const sendNotification = useCallback(async () => { setSendNotificationResult(""); if (!notificationDetails || !context) { return; } try { const response = await fetch("/api/send-notification", { method: "POST", mode: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ fid: context.user.fid, notificationDetails, }), }); if (response.status === 200) { setSendNotificationResult("Success"); return; } else if (response.status === 429) { setSendNotificationResult("Rate limited"); return; } const data = await response.text(); setSendNotificationResult(`Error: ${data}`); } catch (error) { setSendNotificationResult(`Error: ${error}`); } }, [context, notificationDetails]); const sendTx = useCallback(() => { sendTransaction( { // call yoink() on Yoink contract to: "0x4bBFD120d9f352A0BEd7a014bd67913a2007a878", data: "0x9846cd9efc000023c0", }, { onSuccess: (hash) => { setTxHash(hash); }, } ); }, [sendTransaction]); const signTyped = useCallback(() => { signTypedData({ domain: { name: "Frames v2 Demo", version: "1", chainId, }, types: { Message: [{ name: "content", type: "string" }], }, message: { content: "Hello from Frames v2!", }, primaryType: "Message", }); }, [chainId, signTypedData]); const toggleContext = useCallback(() => { setIsContextOpen((prev) => !prev); }, []); if (!isSDKLoaded) { return
Loading...
; } return (

{title}

Context

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

Actions

              sdk.actions.signIn
            
              sdk.actions.openUrl
            
              sdk.actions.openUrl
            
              sdk.actions.close
            

Add to client & notifications

Client fid {context?.client.clientFid}, {context?.client.added ? " frame added to client," : " frame not added to client,"} {notificationDetails ? " notifications enabled" : " notifications disabled"}
              sdk.actions.addFrame
            
{addFrameResult && (
Add frame result: {addFrameResult}
)}
{sendNotificationResult && (
Send notification result: {sendNotificationResult}
)}

Wallet

{address && (
Address:
{truncateAddress(address)}
)} {chainId && (
Chain ID:
{chainId}
)}
{isConnected && ( <>
{isSendTxError && renderError(sendTxError)} {txHash && (
Hash: {truncateAddress(txHash)}
Status:{" "} {isConfirming ? "Confirming..." : isConfirmed ? "Confirmed!" : "Pending"}
)}
{isSignTypedError && renderError(signTypedError)}
{isSwitchChainError && renderError(switchChainError)}
)}
); } function SignMessage() { const { isConnected } = useAccount(); const { connectAsync } = useConnect(); const { signMessage, data: signature, error: signError, isError: isSignError, isPending: isSignPending, } = useSignMessage(); const handleSignMessage = useCallback(async () => { if (!isConnected) { await connectAsync({ chainId: base.id, connector: config.connectors[0], }); } signMessage({ message: "Hello from Frames v2!" }); }, [connectAsync, isConnected, signMessage]); return ( <> {isSignError && renderError(signError)} {signature && (
Signature: {signature}
)} ); } function SendEth() { const { isConnected, chainId } = useAccount(); const { sendTransaction, data, error: sendTxError, isError: isSendTxError, isPending: isSendTxPending, } = useSendTransaction(); const { isLoading: isConfirming, isSuccess: isConfirmed } = useWaitForTransactionReceipt({ hash: data, }); const toAddr = useMemo(() => { // Protocol guild address return chainId === base.id ? "0x32e3C7fD24e175701A35c224f2238d18439C7dBC" : "0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830"; }, [chainId]); const handleSend = useCallback(() => { sendTransaction({ to: toAddr, value: 1n, }); }, [toAddr, sendTransaction]); return ( <> {isSendTxError && renderError(sendTxError)} {data && (
Hash: {truncateAddress(data)}
Status:{" "} {isConfirming ? "Confirming..." : isConfirmed ? "Confirmed!" : "Pending"}
)} ); } function SignIn() { const [signingIn, setSigningIn] = useState(false); const [signingOut, setSigningOut] = useState(false); const [signInResult, setSignInResult] = useState(); const { data: session, status } = useSession() const getNonce = useCallback(async () => { const nonce = await getCsrfToken(); if (!nonce) throw new Error("Unable to generate nonce"); return nonce; }, []); const handleSignIn = useCallback(async () => { try { setSigningIn(true); const nonce = await getNonce(); const result = await sdk.actions.signIn({ nonce }); setSignInResult(result); await signIn("credentials", { message: result.message, signature: result.signature, redirect: false, }); } finally { setSigningIn(false); } }, [getNonce]); const handleSignOut = useCallback(async () => { try { setSigningOut(true); await signOut({ redirect: false }) setSignInResult(undefined); } finally { setSigningOut(false); } }, []); return ( <> {status !== "authenticated" && } {status === "authenticated" && } {session &&
Session
{JSON.stringify(session, null, 2)}
} {signInResult && !signingIn && (
SIWF Result
{JSON.stringify(signInResult, null, 2)}
)} ); } const renderError = (error: Error | null) => { if (!error) return null; if (error instanceof BaseError) { const isUserRejection = error.walk( (e) => e instanceof UserRejectedRequestError ); if (isUserRejection) { return
Rejected by user.
; } } return
{error.message}
; };