diff --git a/bin/init.js b/bin/init.js index ecd213e..d366560 100644 --- a/bin/init.js +++ b/bin/init.js @@ -482,12 +482,6 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe siwe: '^3.0.0', }; - // Add auth-kit and next-auth dependencies if useSponsoredSigner is true - if (answers.useSponsoredSigner) { - packageJson.dependencies['@farcaster/auth-kit'] = '>=0.6.0 <1.0.0'; - packageJson.dependencies['next-auth'] = '^4.24.11'; - } - packageJson.devDependencies = { "@types/inquirer": "^9.0.8", "@types/node": "^20", @@ -655,9 +649,6 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`); } fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`); - if (answers.useSponsoredSigner) { - fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="${answers.useSponsoredSigner}"`); - } fs.unlinkSync(envExamplePath); } else { diff --git a/package.json b/package.json index 5b58bd0..6e3ffdd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.7.4", + "version": "1.7.1", "type": "module", "private": false, "access": "public", diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 44f591d..4c3022f 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -1,19 +1,17 @@ 'use client'; +import '@farcaster/auth-kit/styles.css'; +import { useSignIn, UseSignInData } from '@farcaster/auth-kit'; import { useCallback, useEffect, useState, useRef } from 'react'; -import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; -import { useMiniApp } from '@neynar/react'; +import { cn } from '~/lib/utils'; import { Button } from '~/components/ui/Button'; import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog'; import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton'; +import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog'; import { getItem, removeItem, setItem } from '~/lib/localStorage'; import { useMiniApp } from '@neynar/react'; -import { - signIn as backendSignIn, - signOut as backendSignOut, - useSession, -} from 'next-auth/react'; -import sdk, { SignIn as SignInCore } from '@farcaster/miniapp-sdk'; +import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; +import { useQuickAuth } from '~/hooks/useQuickAuth'; type User = { fid: number; @@ -116,8 +114,69 @@ export function NeynarAuthButton() { const [backendUserProfile, setBackendUserProfile] = useState<{ username?: string; pfpUrl?: string }>({}); // Determine which flow to use based on context + // - Farcaster clients (when context is not undefined): Use QuickAuth (backend flow) + // - Browsers (when context is undefined): Use auth-kit (frontend flow) const useBackendFlow = context !== undefined; + // Helper function to fetch user data from Neynar API + const fetchUserData = useCallback( + async (fid: number): Promise => { + try { + const response = await fetch(`/api/users?fids=${fid}`); + if (response.ok) { + const data = await response.json(); + return data.users?.[0] || null; + } + return null; + } catch (error) { + console.error('Error fetching user data:', error); + return null; + } + }, + [] + ); + + // Auth Kit integration for browser-based authentication (only when not in Farcaster client) + const signInState = useSignIn({ + nonce: !useBackendFlow ? (nonce || undefined) : undefined, + onSuccess: useCallback( + async (res: UseSignInData) => { + if (!useBackendFlow) { + // Only handle localStorage for frontend flow + const existingAuth = getItem(STORAGE_KEY); + const user = res.fid ? await fetchUserData(res.fid) : null; + const authState: StoredAuthState = { + ...existingAuth, + isAuthenticated: true, + user: user as StoredAuthState['user'], + signers: existingAuth?.signers || [], // Preserve existing signers + }; + setItem(STORAGE_KEY, authState); + setStoredAuth(authState); + } + // For backend flow, the session will be handled by QuickAuth + }, + [useBackendFlow, fetchUserData] + ), + onError: useCallback((error?: Error | null) => { + console.error('❌ Sign in error:', error); + }, []), + }); + + const { + signIn: frontendSignIn, + signOut: frontendSignOut, + connect, + reconnect, + isSuccess, + isError, + error, + channelToken, + url, + data, + validSignature, + } = signInState; + // Helper function to create a signer const createSigner = useCallback(async () => { try { @@ -405,18 +464,25 @@ export function NeynarAuthButton() { console.error('❌ Backend sign-in error:', e); } } - }, [nonce, quickAuthSignIn, quickAuthUser, fetchUserData]); + }, [useBackendFlow]); - // Fetch user profile when quickAuthUser.fid changes (for backend flow) + // Auth Kit data synchronization (only for browser flow) useEffect(() => { - if (useBackendFlow && quickAuthUser?.fid) { - (async () => { - const user = await fetchUserData(quickAuthUser.fid); - setBackendUserProfile({ - username: user?.username || '', - pfpUrl: user?.pfp_url || '', - }); - })(); + if (!useBackendFlow) { + setMessage(data?.message || null); + setSignature(data?.signature || null); + + // Reset the signer flow flag when message/signature change + if (data?.message && data?.signature) { + signerFlowStartedRef.current = false; + } + } + }, [useBackendFlow, data?.message, data?.signature]); + + // Connect for frontend flow when nonce is available (only for browser flow) + useEffect(() => { + if (!useBackendFlow && nonce && !channelToken) { + connect(); } }, [useBackendFlow, quickAuthUser?.fid, fetchUserData]); @@ -475,6 +541,97 @@ export function NeynarAuthButton() { } }, [useBackendFlow, pollingInterval, quickAuthSignOut]); + // Backend flow using QuickAuth + const handleBackendSignIn = useCallback(async () => { + if (!nonce) { + console.error('❌ No nonce available for backend sign-in'); + return; + } + + try { + setSignersLoading(true); + const result = await sdk.actions.signIn({ nonce }); + + setMessage(result.message); + setSignature(result.signature); + // Use QuickAuth to sign in + const signInResult = await quickAuthSignIn(); + // Fetch user profile after sign in + if (quickAuthUser?.fid) { + const user = await fetchUserData(quickAuthUser.fid); + setBackendUserProfile({ + username: user?.username || '', + pfpUrl: user?.pfp_url || '', + }); + } + } catch (e) { + if (e instanceof SignInCore.RejectedByUser) { + console.log('ℹ️ Sign-in rejected by user'); + } else { + console.error('❌ Backend sign-in error:', e); + } + } + }, [nonce, quickAuthSignIn, quickAuthUser, fetchUserData]); + + // Fetch user profile when quickAuthUser.fid changes (for backend flow) + useEffect(() => { + if (useBackendFlow && quickAuthUser?.fid) { + (async () => { + const user = await fetchUserData(quickAuthUser.fid); + setBackendUserProfile({ + username: user?.username || '', + pfpUrl: user?.pfp_url || '', + }); + })(); + } + }, [useBackendFlow, quickAuthUser?.fid, fetchUserData]); + + const handleFrontEndSignIn = useCallback(() => { + if (isError) { + reconnect(); + } + setDialogStep('signin'); + setShowDialog(true); + frontendSignIn(); + }, [isError, reconnect, frontendSignIn]); + + const handleSignOut = useCallback(async () => { + try { + setSignersLoading(true); + + if (useBackendFlow) { + // Use QuickAuth sign out + await quickAuthSignOut(); + } else { + // Frontend flow sign out + frontendSignOut(); + removeItem(STORAGE_KEY); + setStoredAuth(null); + } + + // Common cleanup for both flows + setShowDialog(false); + setDialogStep('signin'); + setSignerApprovalUrl(null); + setMessage(null); + setSignature(null); + + // Reset polling interval + if (pollingInterval) { + clearInterval(pollingInterval); + setPollingInterval(null); + } + + // Reset signer flow flag + signerFlowStartedRef.current = false; + } catch (error) { + console.error('❌ Error during sign out:', error); + // Optionally handle error state + } finally { + setSignersLoading(false); + } + }, [useBackendFlow, frontendSignOut, pollingInterval, quickAuthSignOut]); + // Handle fetching signers after successful authentication useEffect(() => { if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) { @@ -551,10 +708,13 @@ export function NeynarAuthButton() { } }, [message, signature]); // Simplified dependencies + // Authentication check based on flow type const authenticated = useBackendFlow - ? !!quickAuthUser?.fid - : storedAuth?.isAuthenticated && !!(storedAuth?.signers && storedAuth.signers.length > 0); + ? !!quickAuthUser?.fid // QuickAuth flow: check if user is authenticated via QuickAuth + : ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && // Auth-kit flow: check auth-kit success or stored auth + !!(storedAuth?.signers && storedAuth.signers.length > 0); // Both flows: ensure signers exist + // User data based on flow type const userData = useBackendFlow ? { fid: quickAuthUser?.fid, @@ -588,7 +748,7 @@ export function NeynarAuthButton() { ) : (