From 4ba948083278067334670b0573b8029755f4f7e3 Mon Sep 17 00:00:00 2001 From: veganbeef Date: Tue, 15 Jul 2025 09:25:08 -0700 Subject: [PATCH] fix: SIWN dependencies --- bin/init.js | 16 +- package.json | 2 +- src/components/ui/NeynarAuthButton/index.tsx | 440 +++++++++---------- src/components/ui/tabs/ActionsTab.tsx | 13 +- 4 files changed, 229 insertions(+), 242 deletions(-) diff --git a/bin/init.js b/bin/init.js index d366560..a151efa 100644 --- a/bin/init.js +++ b/bin/init.js @@ -455,7 +455,6 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe // Add dependencies packageJson.dependencies = { '@farcaster/auth-client': '>=0.3.0 <1.0.0', - '@farcaster/auth-kit': '>=0.6.0 <1.0.0', '@farcaster/miniapp-node': '>=0.1.5 <1.0.0', '@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0', '@farcaster/miniapp-wagmi-connector': '^1.0.0', @@ -482,6 +481,12 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe siwe: '^3.0.0', }; + // Add auth-kit and quick-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", @@ -692,6 +697,15 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.rmSync(binPath, { recursive: true, force: true }); } + // Remove NeynarAuthButton directory if useSponsoredSigner is false + if (!answers.useSponsoredSigner) { + console.log('\nRemoving NeynarAuthButton directory (useSponsoredSigner is false)...'); + const neynarAuthButtonPath = path.join(projectPath, 'src', 'components', 'ui', 'NeynarAuthButton'); + if (fs.existsSync(neynarAuthButtonPath)) { + fs.rmSync(neynarAuthButtonPath, { recursive: true, force: true }); + } + } + // Initialize git repository console.log('\nInitializing git repository...'); execSync('git init', { cwd: projectPath }); diff --git a/package.json b/package.json index 6e3ffdd..c78ab79 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.7.1", + "version": "1.7.2", "type": "module", "private": false, "access": "public", diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index af1c669..3fb3a5d 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -1,16 +1,20 @@ 'use client'; import '@farcaster/auth-kit/styles.css'; -import { useSignIn, UseSignInData } from '@farcaster/auth-kit'; -import { useCallback, useEffect, useState, useRef } from 'react'; +import { useSignIn } from '@farcaster/auth-kit'; +import { useCallback, useEffect, useState } from 'react'; import { cn } from '~/lib/utils'; import { Button } from '~/components/ui/Button'; 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 sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; -import { useQuickAuth } from '~/hooks/useQuickAuth'; +import { + signIn as backendSignIn, + signOut as backendSignOut, + useSession, +} from 'next-auth/react'; +import sdk, { SignIn as SignInCore } from '@farcaster/miniapp-sdk'; type User = { fid: number; @@ -94,8 +98,7 @@ export function NeynarAuthButton() { const [storedAuth, setStoredAuth] = useState(null); const [signersLoading, setSignersLoading] = useState(false); const { context } = useMiniApp(); - const { authenticatedUser: quickAuthUser, signIn: quickAuthSignIn, signOut: quickAuthSignOut } = useQuickAuth(); - + const { data: session } = useSession(); // New state for unified dialog flow const [showDialog, setShowDialog] = useState(false); const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( @@ -109,74 +112,10 @@ export function NeynarAuthButton() { ); const [message, setMessage] = useState(null); const [signature, setSignature] = useState(null); - const [isSignerFlowRunning, setIsSignerFlowRunning] = useState(false); - const signerFlowStartedRef = useRef(false); - 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 { @@ -205,15 +144,43 @@ export function NeynarAuthButton() { if (!useBackendFlow) return; try { - // For backend flow, use QuickAuth to sign in - if (signers && signers.length > 0) { - await quickAuthSignIn(); + // For backend flow, we need to sign in again with the additional data + if (message && signature) { + const signInData = { + message, + signature, + redirect: false, + nonce: nonce || '', + fid: user?.fid?.toString() || '', + signers: JSON.stringify(signers), + user: JSON.stringify(user), + }; + + await backendSignIn('neynar', signInData); } } catch (error) { console.error('❌ Error updating session with signers:', error); } }, - [useBackendFlow, quickAuthSignIn] + [useBackendFlow, message, signature, nonce] + ); + + // 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; + } + }, + [] ); // Helper function to generate signed key request @@ -276,12 +243,10 @@ export function NeynarAuthButton() { if (response.ok) { if (useBackendFlow) { // For backend flow, update session with signers - if (signerData.signers && signerData.signers.length > 0) { - // Get user data for the first signer - let user: StoredAuthState['user'] | null = null; - if (signerData.signers[0].fid) { - user = await fetchUserData(signerData.signers[0].fid) as StoredAuthState['user']; - } + if (signerData.signers && signerData.signers.length > 0) { + const user = + signerData.user || + (await fetchUserData(signerData.signers[0].fid)); await updateSessionWithSigners(signerData.signers, user); } return signerData.signers; @@ -324,46 +289,14 @@ export function NeynarAuthButton() { // Helper function to poll signer status const startPolling = useCallback( (signerUuid: string, message: string, signature: string) => { - // Clear any existing polling interval before starting a new one - if (pollingInterval) { - clearInterval(pollingInterval); - } - - let retryCount = 0; - const maxRetries = 10; // Maximum 10 retries (20 seconds total) - const maxPollingTime = 60000; // Maximum 60 seconds of polling - const startTime = Date.now(); - const interval = setInterval(async () => { - // Check if we've been polling too long - if (Date.now() - startTime > maxPollingTime) { - clearInterval(interval); - setPollingInterval(null); - return; - } - try { const response = await fetch( `/api/auth/signer?signerUuid=${signerUuid}` ); if (!response.ok) { - // Check if it's a rate limit error - if (response.status === 429) { - clearInterval(interval); - setPollingInterval(null); - return; - } - - // Increment retry count for other errors - retryCount++; - if (retryCount >= maxRetries) { - clearInterval(interval); - setPollingInterval(null); - return; - } - - throw new Error(`Failed to poll signer status: ${response.status}`); + throw new Error('Failed to poll signer status'); } const signerData = await response.json(); @@ -385,7 +318,7 @@ export function NeynarAuthButton() { setPollingInterval(interval); }, - [fetchAllSigners, pollingInterval] + [fetchAllSigners] ); // Cleanup polling on unmount @@ -394,7 +327,6 @@ export function NeynarAuthButton() { if (pollingInterval) { clearInterval(pollingInterval); } - signerFlowStartedRef.current = false; }; }, [pollingInterval]); @@ -427,124 +359,68 @@ export function NeynarAuthButton() { } }, [useBackendFlow]); - // Auth Kit data synchronization (only for browser flow) - useEffect(() => { - 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; + // Success callback - this is critical! + const onSuccessCallback = useCallback( + async (res: unknown) => { + if (!useBackendFlow) { + // Only handle localStorage for frontend flow + const existingAuth = getItem(STORAGE_KEY); + const user = await fetchUserData(res.fid); + const authState: StoredAuthState = { + ...existingAuth, + isAuthenticated: true, + user: user as StoredAuthState['user'], + signers: existingAuth?.signers || [], // Preserve existing signers + }; + setItem(STORAGE_KEY, authState); + setStoredAuth(authState); } - } - }, [useBackendFlow, data?.message, data?.signature]); + // For backend flow, the session will be handled by NextAuth + }, + [useBackendFlow, fetchUserData] + ); - // Connect for frontend flow when nonce is available (only for browser flow) + // Error callback + const onErrorCallback = useCallback((error?: Error | null) => { + console.error('❌ Sign in error:', error); + }, []); + + const signInState = useSignIn({ + nonce: nonce || undefined, + onSuccess: onSuccessCallback, + onError: onErrorCallback, + }); + + const { + signIn: frontendSignIn, + signOut: frontendSignOut, + connect, + reconnect, + isSuccess, + isError, + error, + channelToken, + url, + data, + validSignature, + } = signInState; + + useEffect(() => { + setMessage(data?.message || null); + setSignature(data?.signature || null); + }, [data?.message, data?.signature]); + + // Connect for frontend flow when nonce is available useEffect(() => { if (!useBackendFlow && nonce && !channelToken) { connect(); } }, [useBackendFlow, nonce, channelToken, connect]); - // 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) { - signerFlowStartedRef.current = true; - + if (message && signature) { const handleSignerFlow = async () => { - setIsSignerFlowRunning(true); try { const clientContext = context?.client as Record; const isMobileContext = @@ -560,7 +436,6 @@ export function NeynarAuthButton() { // First, fetch existing signers const signers = await fetchAllSigners(message, signature); - if (useBackendFlow && isMobileContext) setSignersLoading(true); // Check if no signers exist or if we have empty signers @@ -581,8 +456,8 @@ export function NeynarAuthButton() { setShowDialog(false); await sdk.actions.openUrl( signedKeyData.signer_approval_url.replace( - 'https://client.farcaster.xyz/deeplinks/signed-key-request', - 'https://farcaster.xyz/~/connect' + 'https://client.farcaster.xyz/deeplinks/', + 'farcaster://' ) ); } else { @@ -605,27 +480,116 @@ export function NeynarAuthButton() { setSignersLoading(false); setShowDialog(false); setSignerApprovalUrl(null); - } finally { - setIsSignerFlowRunning(false); } }; handleSignerFlow(); } - }, [message, signature]); // Simplified dependencies + }, [ + message, + signature, + fetchAllSigners, + createSigner, + generateSignedKeyRequest, + startPolling, + context, + useBackendFlow, + ]); + + // Backend flow using NextAuth + 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 }); + + const signInData = { + message: result.message, + signature: result.signature, + redirect: false, + nonce: nonce, + }; + + const nextAuthResult = await backendSignIn('neynar', signInData); + if (nextAuthResult?.ok) { + setMessage(result.message); + setSignature(result.signature); + } else { + console.error('❌ NextAuth sign-in failed:', nextAuthResult); + } + } catch (e) { + if (e instanceof SignInCore.RejectedByUser) { + console.log('ℹ️ Sign-in rejected by user'); + } else { + console.error('❌ Backend sign-in error:', e); + } + } + }, [nonce]); + + const handleFrontEndSignIn = useCallback(() => { + if (isError) { + reconnect(); + } + setDialogStep('signin'); + setShowDialog(true); + frontendSignIn(); + }, [isError, reconnect, frontendSignIn]); + + const handleSignOut = useCallback(async () => { + try { + setSignersLoading(true); + + if (useBackendFlow) { + // Only sign out from NextAuth if the current session is from Neynar provider + if (session?.provider === 'neynar') { + await backendSignOut({ redirect: false }); + } + } 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); + } + } catch (error) { + console.error('❌ Error during sign out:', error); + // Optionally handle error state + } finally { + setSignersLoading(false); + } + }, [useBackendFlow, frontendSignOut, pollingInterval, session]); - // Authentication check based on flow type const authenticated = useBackendFlow - ? !!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 + ? !!( + session?.provider === 'neynar' && + session?.user?.fid && + session?.signers && + session.signers.length > 0 + ) + : ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && + !!(storedAuth?.signers && storedAuth.signers.length > 0); - // User data based on flow type const userData = useBackendFlow ? { - fid: quickAuthUser?.fid, - username: backendUserProfile.username ?? '', - pfpUrl: backendUserProfile.pfpUrl ?? '', + fid: session?.user?.fid, + username: session?.user?.username || '', + pfpUrl: session?.user?.pfp_url || '', } : { fid: storedAuth?.user?.fid, @@ -654,7 +618,7 @@ export function NeynarAuthButton() { ) : (