mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
feat: add support for coinbase wallet with auto connect and metamask
This commit is contained in:
parent
d8c53ceab7
commit
eda896e478
@ -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:
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,16 +58,12 @@ 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 {
|
||||||
|
|
||||||
if (error instanceof AddFrame.InvalidDomainManifest) {
|
|
||||||
setAddFrameResult(`Not added: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
setAddFrameResult(`Error: ${error}`);
|
setAddFrameResult(`Error: ${error}`);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user