feat: add support for coinbase wallet with auto connect and metamask

This commit is contained in:
veganbeef 2025-05-08 10:47:59 -07:00
parent d8c53ceab7
commit eda896e478
No known key found for this signature in database
6 changed files with 127 additions and 60 deletions

View File

@ -4,6 +4,10 @@ A Farcaster Mini Apps quickstart npx script.
This is a [NextJS](https://nextjs.org/) + TypeScript + React app. 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 ## Getting Started
To create a new frames project, run: To create a new frames project, run:

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.2.18", "version": "1.2.19",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",
@ -31,7 +31,8 @@
"build": "node scripts/build.js", "build": "node scripts/build.js",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"deploy:vercel": "node scripts/deploy.js" "deploy:vercel": "node scripts/deploy.js",
"cleanup": "lsof -ti :3000 | xargs kill -9"
}, },
"bin": { "bin": {
"@neynar/create-farcaster-mini-app": "./bin/index.js" "@neynar/create-farcaster-mini-app": "./bin/index.js"

View File

@ -75,9 +75,7 @@ async function startDev() {
? '1. Run: netstat -ano | findstr :3000\n' + ? '1. Run: netstat -ano | findstr :3000\n' +
'2. Note the PID (Process ID) from the output\n' + '2. Note the PID (Process ID) from the output\n' +
'3. Run: taskkill /PID <PID> /F\n' '3. Run: taskkill /PID <PID> /F\n'
: '1. On macOS/Linux, run: lsof -i :3000\n' + : `On macOS/Linux, run:\nnpm run cleanup\n`) +
'2. Note the PID (Process ID) from the output\n' +
'3. Run: kill -9 <PID>\n') +
'\nThen try running this command again.'); '\nThen try running this command again.');
process.exit(1); process.exit(1);
} }

View File

@ -1,6 +1,6 @@
"use client"; "use client";
import { useCallback, useMemo, useState } from "react"; import { useCallback, useEffect, useMemo, useState } from "react";
import { Input } from "../components/ui/input"; import { Input } from "../components/ui/input";
import { signIn, signOut, getCsrfToken } from "next-auth/react"; import { signIn, signOut, getCsrfToken } from "next-auth/react";
import sdk, { import sdk, {
@ -30,15 +30,22 @@ import { useFrame } from "~/components/providers/FrameProvider";
export default function Demo( export default function Demo(
{ title }: { title?: string } = { title: "Frames v2 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 [isContextOpen, setIsContextOpen] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null); const [txHash, setTxHash] = useState<string | null>(null);
const [sendNotificationResult, setSendNotificationResult] = useState(""); const [sendNotificationResult, setSendNotificationResult] = useState("");
const { address, isConnected } = useAccount(); const { address, isConnected } = useAccount();
const chainId = useChainId(); 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 { const {
sendTransaction, sendTransaction,
error: sendTxError, error: sendTxError,
@ -61,21 +68,6 @@ export default function Demo(
const { disconnect } = useDisconnect(); const { disconnect } = useDisconnect();
const { connect, connectors } = useConnect(); 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 { const {
switchChain, switchChain,
error: switchChainError, error: switchChainError,
@ -101,18 +93,6 @@ export default function Demo(
switchChain({ chainId: nextChain.id }); switchChain({ chainId: nextChain.id });
}, [switchChain, 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 () => { const sendNotification = useCallback(async () => {
setSendNotificationResult(""); setSendNotificationResult("");
if (!notificationDetails || !context) { if (!notificationDetails || !context) {
@ -240,16 +220,7 @@ export default function Demo(
sdk.actions.openUrl sdk.actions.openUrl
</pre> </pre>
</div> </div>
<Button onClick={openUrl}>Open Link</Button> <Button onClick={() => openUrl("https://www.youtube.com/watch?v=dQw4w9WgXcQ")}>Open Link</Button>
</div>
<div className="mb-4">
<div className="p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2">
<pre className="font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-">
sdk.actions.openUrl
</pre>
</div>
<Button onClick={openWarpcastUrl}>Open Warpcast Link</Button>
</div> </div>
<div className="mb-4"> <div className="mb-4">
@ -343,13 +314,30 @@ export default function Demo(
> >
Disconnect Disconnect
</Button> </Button>
) : ( ) : context ? (
/* if context is not null, mini app is running in frame client */
<Button <Button
onClick={handleConnect} onClick={() => connect({ connector: connectors[0] })}
className="w-full" className="w-full"
> >
Connect Connect
</Button> </Button>
) : (
/* if context is null, mini app is running in browser */
<div className="space-y-2">
<Button
onClick={() => connect({ connector: connectors[1] })}
className="w-full"
>
Connect Coinbase Wallet
</Button>
<Button
onClick={() => connect({ connector: connectors[2] })}
className="w-full"
>
Connect MetaMask
</Button>
</div>
)} )}
</div> </div>
@ -630,6 +618,7 @@ function ViewProfile() {
</> </>
); );
} }
const renderError = (error: Error | null) => { const renderError = (error: Error | null) => {
if (!error) return null; if (!error) return null;
if (error instanceof BaseError) { if (error instanceof BaseError) {

View File

@ -8,6 +8,13 @@ import React from "react";
interface FrameContextType { interface FrameContextType {
isSDKLoaded: boolean; isSDKLoaded: boolean;
context: Context.FrameContext | undefined; context: Context.FrameContext | undefined;
openUrl: (url: string) => Promise<void>;
close: () => Promise<void>;
added: boolean;
notificationDetails: FrameNotificationDetails | null;
lastEvent: string;
addFrame: () => Promise<void>;
addFrameResult: string;
} }
const FrameContext = React.createContext<FrameContextType | undefined>(undefined); const FrameContext = React.createContext<FrameContextType | undefined>(undefined);
@ -20,10 +27,26 @@ export function useFrame() {
const [lastEvent, setLastEvent] = useState(""); const [lastEvent, setLastEvent] = useState("");
const [addFrameResult, setAddFrameResult] = 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 () => { const addFrame = useCallback(async () => {
try { try {
setNotificationDetails(null); setNotificationDetails(null);
const result = await sdk.actions.addFrame(); const result = await sdk.actions.addFrame();
if (result.notificationDetails) { if (result.notificationDetails) {
@ -35,15 +58,11 @@ export function useFrame() {
: "Added, got no notification details" : "Added, got no notification details"
); );
} catch (error) { } catch (error) {
if (error instanceof AddFrame.RejectedByUser) { if (error instanceof AddFrame.RejectedByUser || error instanceof AddFrame.InvalidDomainManifest) {
setAddFrameResult(`Not added: ${error.message}`); 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]); }, [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 }) { export function FrameProvider({ children }: { children: React.ReactNode }) {
const { isSDKLoaded, context } = useFrame(); const frameContext = useFrame();
if (!isSDKLoaded) { if (!frameContext.isSDKLoaded) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return ( return (
<FrameContext.Provider value={{ isSDKLoaded, context }}> <FrameContext.Provider value={frameContext}>
{children} {children}
</FrameContext.Provider> </FrameContext.Provider>
); );

View File

@ -4,6 +4,42 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { farcasterFrame } from "@farcaster/frame-wagmi-connector"; import { farcasterFrame } from "@farcaster/frame-wagmi-connector";
import { coinbaseWallet, metaMask } from 'wagmi/connectors'; import { coinbaseWallet, metaMask } from 'wagmi/connectors';
import { APP_NAME, APP_ICON_URL, APP_URL } from "~/lib/constants"; 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({ export const config = createConfig({
chains: [base, optimism, mainnet, degen, unichain], chains: [base, optimism, mainnet, degen, unichain],
@ -32,10 +68,20 @@ export const config = createConfig({
const queryClient = new QueryClient(); 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 }) { export default function Provider({ children }: { children: React.ReactNode }) {
return ( return (
<WagmiProvider config={config}> <WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider> <QueryClientProvider client={queryClient}>
<CoinbaseWalletAutoConnect>
{children}
</CoinbaseWalletAutoConnect>
</QueryClientProvider>
</WagmiProvider> </WagmiProvider>
); );
} }