From 349cdea489c966ee1d85935041ea2872b7a81197 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 16 Jul 2025 17:20:36 +0530 Subject: [PATCH] Revert "Merge pull request #18 from neynarxyz/veganbeef/fix-siwn" This reverts commit 78626c2dc7d4646bdd9b60b8f67f3ffe5c1defb5, reversing changes made to b1fdfc19a92241638692d58494f48ce1bb25df74. --- bin/init.js | 10 +- src/components/ui/NeynarAuthButton/index.tsx | 386 +++++++++---------- src/components/ui/tabs/ActionsTab.tsx | 13 +- 3 files changed, 188 insertions(+), 221 deletions(-) diff --git a/bin/init.js b/bin/init.js index d32982a..1d9e902 100644 --- a/bin/init.js +++ b/bin/init.js @@ -460,6 +460,7 @@ export async function init( // 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', @@ -707,15 +708,6 @@ export async function init( 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/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index b91fc5a..2f1d8ad 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -1,9 +1,8 @@ 'use client'; -import '@farcaster/auth-kit/styles.css'; -import { useSignIn } from '@farcaster/auth-kit'; -import { useCallback, useEffect, useState } from 'react'; -import { cn } from '~/lib/utils'; +import { useCallback, useEffect, useState, useRef } from 'react'; +import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; +import { useMiniApp } from '@neynar/react'; import { Button } from '~/components/ui/Button'; import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog'; import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton'; @@ -24,7 +23,6 @@ type User = { // Add other user properties as needed }; -const STORAGE_KEY = 'neynar_authenticated_user'; const FARCASTER_FID = 9152; interface StoredAuthState { @@ -98,7 +96,12 @@ export function NeynarAuthButton() { const [storedAuth, setStoredAuth] = useState(null); const [signersLoading, setSignersLoading] = useState(false); const { context } = useMiniApp(); - const { data: session } = useSession(); + const { + authenticatedUser: quickAuthUser, + signIn: quickAuthSignIn, + signOut: quickAuthSignOut, + } = useQuickAuth(); + // New state for unified dialog flow const [showDialog, setShowDialog] = useState(false); const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( @@ -112,6 +115,12 @@ 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 const useBackendFlow = context !== undefined; @@ -144,25 +153,15 @@ export function NeynarAuthButton() { if (!useBackendFlow) return; try { - // 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); + // For backend flow, use QuickAuth to sign in + if (signers && signers.length > 0) { + await quickAuthSignIn(); } } catch (error) { console.error('❌ Error updating session with signers:', error); } }, - [useBackendFlow, message, signature, nonce] + [useBackendFlow, quickAuthSignIn], ); // Helper function to fetch user data from Neynar API @@ -244,14 +243,18 @@ export function NeynarAuthButton() { if (useBackendFlow) { // For backend flow, update session with signers if (signerData.signers && signerData.signers.length > 0) { - const user = - signerData.user || - (await fetchUserData(signerData.signers[0].fid)); + // 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']; + } await updateSessionWithSigners(signerData.signers, user); } return signerData.signers; } else { - // For frontend flow, store in localStorage + // For frontend flow, store in memory only let user: StoredAuthState['user'] | null = null; if (signerData.signers && signerData.signers.length > 0) { @@ -261,13 +264,12 @@ export function NeynarAuthButton() { user = fetchedUser; } - // Store signers in localStorage, preserving existing auth data + // Store signers in memory only const updatedState: StoredAuthState = { isAuthenticated: !!user, signers: signerData.signers || [], user, }; - setItem(STORAGE_KEY, updatedState); setStoredAuth(updatedState); return signerData.signers; @@ -289,14 +291,46 @@ 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) { - throw new Error('Failed to poll signer status'); + // 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}`); } const signerData = await response.json(); @@ -318,7 +352,7 @@ export function NeynarAuthButton() { setPollingInterval(interval); }, - [fetchAllSigners] + [fetchAllSigners, pollingInterval], ); // Cleanup polling on unmount @@ -327,6 +361,7 @@ export function NeynarAuthButton() { if (pollingInterval) { clearInterval(pollingInterval); } + signerFlowStartedRef.current = false; }; }, [pollingInterval]); @@ -349,78 +384,118 @@ export function NeynarAuthButton() { generateNonce(); }, []); - // Load stored auth state on mount (only for frontend flow) - useEffect(() => { - if (!useBackendFlow) { - const stored = getItem(STORAGE_KEY); - if (stored && stored.isAuthenticated) { - setStoredAuth(stored); + // 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 + 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); } } - }, [useBackendFlow]); - - // 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); - } - // For backend flow, the session will be handled by NextAuth - }, - [useBackendFlow, fetchUserData] - ); - - // 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; + }, [nonce, quickAuthSignIn, quickAuthUser, fetchUserData]); + // Fetch user profile when quickAuthUser.fid changes (for backend flow) 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(); + if (useBackendFlow && quickAuthUser?.fid) { + (async () => { + const user = await fetchUserData(quickAuthUser.fid); + setBackendUserProfile({ + username: user?.username || '', + pfpUrl: user?.pfp_url || '', + }); + })(); } - }, [useBackendFlow, nonce, channelToken, connect]); + }, [useBackendFlow, quickAuthUser?.fid, fetchUserData]); + + const handleFrontEndSignIn = useCallback(async () => { + try { + setSignersLoading(true); + const result = await sdk.actions.signIn({ nonce: nonce || '' }); + + setMessage(result.message); + setSignature(result.signature); + + // For frontend flow, we'll handle the signer flow in the useEffect + } catch (e) { + if (e instanceof SignInCore.RejectedByUser) { + console.log('ℹ️ Sign-in rejected by user'); + } else { + console.error('❌ Frontend sign-in error:', e); + } + } finally { + setSignersLoading(false); + } + }, [nonce]); + + const handleSignOut = useCallback(async () => { + try { + setSignersLoading(true); + + if (useBackendFlow) { + // Use QuickAuth sign out + await quickAuthSignOut(); + } else { + // Frontend flow sign out + 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, pollingInterval, quickAuthSignOut]); // Handle fetching signers after successful authentication useEffect(() => { - if (message && signature) { + if ( + message && + signature && + !isSignerFlowRunning && + !signerFlowStartedRef.current + ) { + signerFlowStartedRef.current = true; + const handleSignerFlow = async () => { + setIsSignerFlowRunning(true); try { const clientContext = context?.client as Record; const isMobileContext = @@ -436,6 +511,7 @@ 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 @@ -456,9 +532,9 @@ export function NeynarAuthButton() { setShowDialog(false); await sdk.actions.openUrl( signedKeyData.signer_approval_url.replace( - 'https://client.farcaster.xyz/deeplinks/', - 'farcaster://' - ) + 'https://client.farcaster.xyz/deeplinks/signed-key-request', + 'https://farcaster.xyz/~/connect', + ), ); } else { setShowDialog(true); // Ensure dialog is shown during loading @@ -480,116 +556,25 @@ export function NeynarAuthButton() { setSignersLoading(false); setShowDialog(false); setSignerApprovalUrl(null); + } finally { + setIsSignerFlowRunning(false); } }; handleSignerFlow(); } - }, [ - 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]); + }, [message, signature]); // Simplified dependencies const authenticated = useBackendFlow - ? !!( - session?.provider === 'neynar' && - session?.user?.fid && - session?.signers && - session.signers.length > 0 - ) - : ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && + ? !!quickAuthUser?.fid + : storedAuth?.isAuthenticated && !!(storedAuth?.signers && storedAuth.signers.length > 0); const userData = useBackendFlow ? { - fid: session?.user?.fid, - username: session?.user?.username || '', - pfpUrl: session?.user?.pfp_url || '', + fid: quickAuthUser?.fid, + username: backendUserProfile.username ?? '', + pfpUrl: backendUserProfile.pfpUrl ?? '', } : { fid: storedAuth?.user?.fid, @@ -618,18 +603,17 @@ export function NeynarAuthButton() { ) : (