From 16c433a13c1cd886dee50ddfa2e1b94c6874bf3d Mon Sep 17 00:00:00 2001 From: veganbeef Date: Mon, 14 Jul 2025 13:24:55 -0700 Subject: [PATCH 1/2] fix: add back auth-kit --- src/components/ui/NeynarAuthButton/index.tsx | 169 +++++++++++++------ 1 file changed, 121 insertions(+), 48 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index fd1f8c3..af1c669 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -1,10 +1,13 @@ 'use client'; +import '@farcaster/auth-kit/styles.css'; +import { useSignIn, UseSignInData } from '@farcaster/auth-kit'; import { useCallback, useEffect, useState, useRef } 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'; @@ -17,6 +20,7 @@ type User = { // Add other user properties as needed }; +const STORAGE_KEY = 'neynar_authenticated_user'; const FARCASTER_FID = 9152; interface StoredAuthState { @@ -110,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 { @@ -151,24 +216,6 @@ export function NeynarAuthButton() { [useBackendFlow, quickAuthSignIn] ); - // 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 const generateSignedKeyRequest = useCallback( async (signerUuid: string, publicKey: string) => { @@ -239,7 +286,7 @@ export function NeynarAuthButton() { } return signerData.signers; } else { - // For frontend flow, store in memory only + // For frontend flow, store in localStorage let user: StoredAuthState['user'] | null = null; if (signerData.signers && signerData.signers.length > 0) { @@ -249,12 +296,13 @@ export function NeynarAuthButton() { user = fetchedUser; } - // Store signers in memory only + // Store signers in localStorage, preserving existing auth data const updatedState: StoredAuthState = { isAuthenticated: !!user, signers: signerData.signers || [], user, }; + setItem(STORAGE_KEY, updatedState); setStoredAuth(updatedState); return signerData.signers; @@ -369,6 +417,36 @@ 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); + } + } + }, [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; + } + } + }, [useBackendFlow, data?.message, data?.signature]); + + // Connect for frontend flow when nonce is available (only for browser flow) + useEffect(() => { + if (!useBackendFlow && nonce && !channelToken) { + connect(); + } + }, [useBackendFlow, nonce, channelToken, connect]); + // Backend flow using QuickAuth const handleBackendSignIn = useCallback(async () => { if (!nonce) { @@ -414,25 +492,14 @@ export function NeynarAuthButton() { } }, [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); + const handleFrontEndSignIn = useCallback(() => { + if (isError) { + reconnect(); } - }, [nonce]); + setDialogStep('signin'); + setShowDialog(true); + frontendSignIn(); + }, [isError, reconnect, frontendSignIn]); const handleSignOut = useCallback(async () => { try { @@ -443,6 +510,8 @@ export function NeynarAuthButton() { await quickAuthSignOut(); } else { // Frontend flow sign out + frontendSignOut(); + removeItem(STORAGE_KEY); setStoredAuth(null); } @@ -467,7 +536,7 @@ export function NeynarAuthButton() { } finally { setSignersLoading(false); } - }, [useBackendFlow, pollingInterval, quickAuthSignOut]); + }, [useBackendFlow, frontendSignOut, pollingInterval, quickAuthSignOut]); // Handle fetching signers after successful authentication useEffect(() => { @@ -545,10 +614,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, @@ -582,17 +654,18 @@ export function NeynarAuthButton() { ) : (