From eda896e478734cf8586eba7d47d39c0b913a0d02 Mon Sep 17 00:00:00 2001 From: veganbeef Date: Thu, 8 May 2025 10:47:59 -0700 Subject: [PATCH] feat: add support for coinbase wallet with auto connect and metamask --- README.md | 4 ++ package.json | 5 +- scripts/dev.js | 4 +- src/components/Demo.tsx | 73 +++++++++------------- src/components/providers/FrameProvider.tsx | 53 ++++++++++++---- src/components/providers/WagmiProvider.tsx | 48 +++++++++++++- 6 files changed, 127 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index dbd7cb7..fa2c92c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,10 @@ A Farcaster Mini Apps quickstart npx script. This is a [NextJS](https://nextjs.org/) + TypeScript + React app. +## Guide + +Check out [this Neynar docs page](https://docs.neynar.com/docs/create-v2-farcaster-frame-in-60s) for a simple guide on how to create a Farcaster Mini App in less than 60 seconds! + ## Getting Started To create a new frames project, run: diff --git a/package.json b/package.json index 8d58fe9..6ba8e42 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.2.18", + "version": "1.2.19", "type": "module", "private": false, "access": "public", @@ -31,7 +31,8 @@ "build": "node scripts/build.js", "start": "next start", "lint": "next lint", - "deploy:vercel": "node scripts/deploy.js" + "deploy:vercel": "node scripts/deploy.js", + "cleanup": "lsof -ti :3000 | xargs kill -9" }, "bin": { "@neynar/create-farcaster-mini-app": "./bin/index.js" diff --git a/scripts/dev.js b/scripts/dev.js index c1f9a00..58546f0 100755 --- a/scripts/dev.js +++ b/scripts/dev.js @@ -75,9 +75,7 @@ async function startDev() { ? '1. Run: netstat -ano | findstr :3000\n' + '2. Note the PID (Process ID) from the output\n' + '3. Run: taskkill /PID /F\n' - : '1. On macOS/Linux, run: lsof -i :3000\n' + - '2. Note the PID (Process ID) from the output\n' + - '3. Run: kill -9 \n') + + : `On macOS/Linux, run:\nnpm run cleanup\n`) + '\nThen try running this command again.'); process.exit(1); } diff --git a/src/components/Demo.tsx b/src/components/Demo.tsx index 6464ed2..4195087 100644 --- a/src/components/Demo.tsx +++ b/src/components/Demo.tsx @@ -1,6 +1,6 @@ "use client"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { Input } from "../components/ui/input"; import { signIn, signOut, getCsrfToken } from "next-auth/react"; import sdk, { @@ -30,15 +30,22 @@ import { useFrame } from "~/components/providers/FrameProvider"; export default function Demo( { title }: { title?: string } = { title: "Frames v2 Demo" } ) { - const { isSDKLoaded, context, added, notificationDetails, lastEvent, addFrame, addFrameResult } = useFrame(); + const { isSDKLoaded, context, added, notificationDetails, lastEvent, addFrame, addFrameResult, openUrl, close } = useFrame(); const [isContextOpen, setIsContextOpen] = useState(false); const [txHash, setTxHash] = useState(null); - const [sendNotificationResult, setSendNotificationResult] = useState(""); const { address, isConnected } = useAccount(); const chainId = useChainId(); + useEffect(() => { + console.log("isSDKLoaded", isSDKLoaded); + console.log("context", context); + console.log("address", address); + console.log("isConnected", isConnected); + console.log("chainId", chainId); + }, [context, address, isConnected, chainId, isSDKLoaded]); + const { sendTransaction, error: sendTxError, @@ -61,21 +68,6 @@ export default function Demo( const { disconnect } = useDisconnect(); const { connect, connectors } = useConnect(); - const handleConnect = useCallback(async () => { - if (context) { - // If we're in a frame client, use the frame connector - await connect({ connector: connectors[0] }); - } else { - try { - // Try Coinbase Wallet first - await connect({ connector: connectors[1] }); - } catch (error) { - // If Coinbase Wallet fails, try MetaMask - await connect({ connector: connectors[2] }); - } - } - }, [connect, connectors, context]); - const { switchChain, error: switchChainError, @@ -101,18 +93,6 @@ export default function Demo( switchChain({ chainId: nextChain.id }); }, [switchChain, nextChain.id]); - 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 sendNotification = useCallback(async () => { setSendNotificationResult(""); if (!notificationDetails || !context) { @@ -240,16 +220,7 @@ export default function Demo( sdk.actions.openUrl - - - -
-
-
-                sdk.actions.openUrl
-              
-
- +
@@ -343,13 +314,30 @@ export default function Demo( > Disconnect - ) : ( + ) : context ? ( + /* if context is not null, mini app is running in frame client */ + ) : ( + /* if context is null, mini app is running in browser */ +
+ + +
)}
@@ -630,6 +618,7 @@ function ViewProfile() { ); } + const renderError = (error: Error | null) => { if (!error) return null; if (error instanceof BaseError) { diff --git a/src/components/providers/FrameProvider.tsx b/src/components/providers/FrameProvider.tsx index c18d512..18d2c87 100644 --- a/src/components/providers/FrameProvider.tsx +++ b/src/components/providers/FrameProvider.tsx @@ -8,6 +8,13 @@ import React from "react"; interface FrameContextType { isSDKLoaded: boolean; context: Context.FrameContext | undefined; + openUrl: (url: string) => Promise; + close: () => Promise; + added: boolean; + notificationDetails: FrameNotificationDetails | null; + lastEvent: string; + addFrame: () => Promise; + addFrameResult: string; } const FrameContext = React.createContext(undefined); @@ -20,10 +27,26 @@ export function useFrame() { const [lastEvent, setLastEvent] = useState(""); const [addFrameResult, setAddFrameResult] = useState(""); + // SDK actions only work in mini app clients, so this pattern supports browser actions as well + const openUrl = useCallback(async (url: string) => { + if (context) { + await sdk.actions.openUrl(url); + } else { + window.open(url, '_blank'); + } + }, [context]); + + const close = useCallback(async () => { + if (context) { + await sdk.actions.close(); + } else { + window.close(); + } + }, [context]); + const addFrame = useCallback(async () => { try { setNotificationDetails(null); - const result = await sdk.actions.addFrame(); if (result.notificationDetails) { @@ -35,15 +58,11 @@ export function useFrame() { : "Added, got no notification details" ); } catch (error) { - if (error instanceof AddFrame.RejectedByUser) { + if (error instanceof AddFrame.RejectedByUser || error instanceof AddFrame.InvalidDomainManifest) { setAddFrameResult(`Not added: ${error.message}`); + }else { + setAddFrameResult(`Error: ${error}`); } - - if (error instanceof AddFrame.InvalidDomainManifest) { - setAddFrameResult(`Not added: ${error.message}`); - } - - setAddFrameResult(`Error: ${error}`); } }, []); @@ -111,18 +130,28 @@ export function useFrame() { } }, [isSDKLoaded]); - return { isSDKLoaded, context, added, notificationDetails, lastEvent, addFrame, addFrameResult }; + return { + isSDKLoaded, + context, + added, + notificationDetails, + lastEvent, + addFrame, + addFrameResult, + openUrl, + close, + }; } export function FrameProvider({ children }: { children: React.ReactNode }) { - const { isSDKLoaded, context } = useFrame(); + const frameContext = useFrame(); - if (!isSDKLoaded) { + if (!frameContext.isSDKLoaded) { return
Loading...
; } return ( - + {children} ); diff --git a/src/components/providers/WagmiProvider.tsx b/src/components/providers/WagmiProvider.tsx index e81bcb3..f1472d5 100644 --- a/src/components/providers/WagmiProvider.tsx +++ b/src/components/providers/WagmiProvider.tsx @@ -4,6 +4,42 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { farcasterFrame } from "@farcaster/frame-wagmi-connector"; import { coinbaseWallet, metaMask } from 'wagmi/connectors'; import { APP_NAME, APP_ICON_URL, APP_URL } from "~/lib/constants"; +import { useEffect, useState } from "react"; +import { useConnect, useAccount } from "wagmi"; +import React from "react"; + +// Custom hook for Coinbase Wallet detection and auto-connection +function useCoinbaseWalletAutoConnect() { + const [isCoinbaseWallet, setIsCoinbaseWallet] = useState(false); + const { connect, connectors } = useConnect(); + const { isConnected } = useAccount(); + + useEffect(() => { + // Check if we're running in Coinbase Wallet + const checkCoinbaseWallet = () => { + const isInCoinbaseWallet = window.ethereum?.isCoinbaseWallet || + window.ethereum?.isCoinbaseWalletExtension || + window.ethereum?.isCoinbaseWalletBrowser; + setIsCoinbaseWallet(!!isInCoinbaseWallet); + }; + + checkCoinbaseWallet(); + window.addEventListener('ethereum#initialized', checkCoinbaseWallet); + + return () => { + window.removeEventListener('ethereum#initialized', checkCoinbaseWallet); + }; + }, []); + + useEffect(() => { + // Auto-connect if in Coinbase Wallet and not already connected + if (isCoinbaseWallet && !isConnected) { + connect({ connector: connectors[1] }); // Coinbase Wallet connector + } + }, [isCoinbaseWallet, isConnected, connect, connectors]); + + return isCoinbaseWallet; +} export const config = createConfig({ chains: [base, optimism, mainnet, degen, unichain], @@ -32,10 +68,20 @@ export const config = createConfig({ const queryClient = new QueryClient(); +// Wrapper component that provides Coinbase Wallet auto-connection +function CoinbaseWalletAutoConnect({ children }: { children: React.ReactNode }) { + useCoinbaseWalletAutoConnect(); + return <>{children}; +} + export default function Provider({ children }: { children: React.ReactNode }) { return ( - {children} + + + {children} + + ); }