From 349cdea489c966ee1d85935041ea2872b7a81197 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 16 Jul 2025 17:20:36 +0530 Subject: [PATCH 1/8] 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() { ) : ( - {/* Notification functionality */} {notificationState.sendStatus && ( -
+
Send notification result: {notificationState.sendStatus}
)} @@ -174,24 +174,24 @@ export function ActionsTab() { {/* Haptic feedback controls */} -
-
); -} +} \ No newline at end of file diff --git a/src/components/ui/tabs/HomeTab.tsx b/src/components/ui/tabs/HomeTab.tsx index 058465d..eb139cc 100644 --- a/src/components/ui/tabs/HomeTab.tsx +++ b/src/components/ui/tabs/HomeTab.tsx @@ -1,12 +1,12 @@ -'use client'; +"use client"; /** * HomeTab component displays the main landing content for the mini app. - * + * * This is the default tab that users see when they first open the mini app. * It provides a simple welcome message and placeholder content that can be * customized for specific use cases. - * + * * @example * ```tsx * @@ -17,10 +17,8 @@ export function HomeTab() {

Put your content here!

-

- Powered by Neynar 🪐 -

+

Powered by Neynar 🪐

); -} +} \ No newline at end of file diff --git a/src/components/ui/tabs/WalletTab.tsx b/src/components/ui/tabs/WalletTab.tsx index cac9f7e..bf439c7 100644 --- a/src/components/ui/tabs/WalletTab.tsx +++ b/src/components/ui/tabs/WalletTab.tsx @@ -1,32 +1,22 @@ -'use client'; +"use client"; -import { useCallback, useMemo, useState, useEffect } from 'react'; -import { useMiniApp } from '@neynar/react'; +import { useCallback, useMemo, useState, useEffect } from "react"; +import { useAccount, useSendTransaction, useSignTypedData, useWaitForTransactionReceipt, useDisconnect, useConnect, useSwitchChain, useChainId, type Connector } from "wagmi"; import { useWallet as useSolanaWallet } from '@solana/wallet-adapter-react'; -import { - useAccount, - useSendTransaction, - useSignTypedData, - useWaitForTransactionReceipt, - useDisconnect, - useConnect, - useSwitchChain, - useChainId, - type Connector, -} from 'wagmi'; -import { base, degen, mainnet, optimism, unichain } from 'wagmi/chains'; -import { USE_WALLET, APP_NAME } from '../../../lib/constants'; -import { renderError } from '../../../lib/errorUtils'; -import { truncateAddress } from '../../../lib/truncateAddress'; -import { Button } from '../Button'; -import { SendEth } from '../wallet/SendEth'; -import { SendSolana } from '../wallet/SendSolana'; -import { SignEvmMessage } from '../wallet/SignEvmMessage'; -import { SignSolanaMessage } from '../wallet/SignSolanaMessage'; +import { base, degen, mainnet, optimism, unichain } from "wagmi/chains"; +import { Button } from "../Button"; +import { truncateAddress } from "../../../lib/truncateAddress"; +import { renderError } from "../../../lib/errorUtils"; +import { SignEvmMessage } from "../wallet/SignEvmMessage"; +import { SendEth } from "../wallet/SendEth"; +import { SignSolanaMessage } from "../wallet/SignSolanaMessage"; +import { SendSolana } from "../wallet/SendSolana"; +import { USE_WALLET, APP_NAME } from "../../../lib/constants"; +import { useMiniApp } from "@neynar/react"; /** * WalletTab component manages wallet-related UI for both EVM and Solana chains. - * + * * This component provides a comprehensive wallet interface that supports: * - EVM wallet connections (Farcaster Frame, Coinbase Wallet, MetaMask) * - Solana wallet integration @@ -34,10 +24,10 @@ import { SignSolanaMessage } from '../wallet/SignSolanaMessage'; * - Transaction sending for both chains * - Chain switching for EVM chains * - Auto-connection in Farcaster clients - * + * * The component automatically detects when running in a Farcaster client * and attempts to auto-connect using the Farcaster Frame connector. - * + * * @example * ```tsx * @@ -57,8 +47,7 @@ function WalletStatus({ address, chainId }: WalletStatusProps) { <> {address && (
- Address:{' '} -
{truncateAddress(address)}
+ Address:
{truncateAddress(address)}
)} {chainId && ( @@ -101,14 +90,13 @@ function ConnectionControls({ if (context) { return (
- -
@@ -138,10 +120,8 @@ function ConnectionControls({ export function WalletTab() { // --- State --- - const [evmContractTransactionHash, setEvmContractTransactionHash] = useState< - string | null - >(null); - + const [evmContractTransactionHash, setEvmContractTransactionHash] = useState(null); + // --- Hooks --- const { context } = useMiniApp(); const { address, isConnected } = useAccount(); @@ -157,12 +137,10 @@ export function WalletTab() { isPending: isEvmTransactionPending, } = useSendTransaction(); - const { - isLoading: isEvmTransactionConfirming, - isSuccess: isEvmTransactionConfirmed, - } = useWaitForTransactionReceipt({ - hash: evmContractTransactionHash as `0x${string}`, - }); + const { isLoading: isEvmTransactionConfirming, isSuccess: isEvmTransactionConfirmed } = + useWaitForTransactionReceipt({ + hash: evmContractTransactionHash as `0x${string}`, + }); const { signTypedData, @@ -184,32 +162,38 @@ export function WalletTab() { // --- Effects --- /** * Auto-connect when Farcaster context is available. - * + * * This effect detects when the app is running in a Farcaster client * and automatically attempts to connect using the Farcaster Frame connector. * It includes comprehensive logging for debugging connection issues. */ useEffect(() => { // Check if we're in a Farcaster client environment - const isInFarcasterClient = - typeof window !== 'undefined' && - (window.location.href.includes('warpcast.com') || - window.location.href.includes('farcaster') || - window.ethereum?.isFarcaster || - context?.client); - - if ( - context?.user?.fid && - !isConnected && - connectors.length > 0 && - isInFarcasterClient - ) { + const isInFarcasterClient = typeof window !== 'undefined' && + (window.location.href.includes('warpcast.com') || + window.location.href.includes('farcaster') || + window.ethereum?.isFarcaster || + context?.client); + + if (context?.user?.fid && !isConnected && connectors.length > 0 && isInFarcasterClient) { + console.log("Attempting auto-connection with Farcaster context..."); + console.log("- User FID:", context.user.fid); + console.log("- Available connectors:", connectors.map((c, i) => `${i}: ${c.name}`)); + console.log("- Using connector:", connectors[0].name); + console.log("- In Farcaster client:", isInFarcasterClient); + // Use the first connector (farcasterFrame) for auto-connection try { connect({ connector: connectors[0] }); } catch (error) { - console.error('Auto-connection failed:', error); + console.error("Auto-connection failed:", error); } + } else { + console.log("Auto-connection conditions not met:"); + console.log("- Has context:", !!context?.user?.fid); + console.log("- Is connected:", isConnected); + console.log("- Has connectors:", connectors.length > 0); + console.log("- In Farcaster client:", isInFarcasterClient); } }, [context?.user?.fid, isConnected, connectors, connect, context?.client]); @@ -243,7 +227,7 @@ export function WalletTab() { /** * Sends a transaction to call the yoink() function on the Yoink contract. - * + * * This function sends a transaction to a specific contract address with * the encoded function call data for the yoink() function. */ @@ -251,20 +235,20 @@ export function WalletTab() { sendTransaction( { // call yoink() on Yoink contract - to: '0x4bBFD120d9f352A0BEd7a014bd67913a2007a878', - data: '0x9846cd9efc000023c0', + to: "0x4bBFD120d9f352A0BEd7a014bd67913a2007a878", + data: "0x9846cd9efc000023c0", }, { - onSuccess: hash => { + onSuccess: (hash) => { setEvmContractTransactionHash(hash); }, - }, + } ); }, [sendTransaction]); /** * Signs typed data using EIP-712 standard. - * + * * This function creates a typed data structure with the app name, version, * and chain ID, then requests the user to sign it. */ @@ -272,16 +256,16 @@ export function WalletTab() { signTypedData({ domain: { name: APP_NAME, - version: '1', + version: "1", chainId, }, types: { - Message: [{ name: 'content', type: 'string' }], + Message: [{ name: "content", type: "string" }], }, message: { content: `Hello from ${APP_NAME}!`, }, - primaryType: 'Message', + primaryType: "Message", }); }, [chainId, signTypedData]); @@ -324,12 +308,12 @@ export function WalletTab() {
Hash: {truncateAddress(evmContractTransactionHash)}
- Status:{' '} + Status:{" "} {isEvmTransactionConfirming - ? 'Confirming...' + ? "Confirming..." : isEvmTransactionConfirmed - ? 'Confirmed!' - : 'Pending'} + ? "Confirmed!" + : "Pending"}
)} @@ -363,4 +347,4 @@ export function WalletTab() { )}
); -} +} \ No newline at end of file diff --git a/src/components/ui/tabs/index.ts b/src/components/ui/tabs/index.ts index d4b044f..09492dd 100644 --- a/src/components/ui/tabs/index.ts +++ b/src/components/ui/tabs/index.ts @@ -1,4 +1,4 @@ export { HomeTab } from './HomeTab'; export { ActionsTab } from './ActionsTab'; export { ContextTab } from './ContextTab'; -export { WalletTab } from './WalletTab'; +export { WalletTab } from './WalletTab'; \ No newline at end of file diff --git a/src/components/ui/wallet/SendEth.tsx b/src/components/ui/wallet/SendEth.tsx index 45afd29..2d20da1 100644 --- a/src/components/ui/wallet/SendEth.tsx +++ b/src/components/ui/wallet/SendEth.tsx @@ -1,29 +1,25 @@ -'use client'; +"use client"; -import { useCallback, useMemo } from 'react'; -import { - useAccount, - useSendTransaction, - useWaitForTransactionReceipt, -} from 'wagmi'; -import { base } from 'wagmi/chains'; -import { renderError } from '../../../lib/errorUtils'; -import { truncateAddress } from '../../../lib/truncateAddress'; -import { Button } from '../Button'; +import { useCallback, useMemo } from "react"; +import { useAccount, useSendTransaction, useWaitForTransactionReceipt } from "wagmi"; +import { base } from "wagmi/chains"; +import { Button } from "../Button"; +import { truncateAddress } from "../../../lib/truncateAddress"; +import { renderError } from "../../../lib/errorUtils"; /** * SendEth component handles sending ETH transactions to protocol guild addresses. - * + * * This component provides a simple interface for users to send small amounts * of ETH to protocol guild addresses. It automatically selects the appropriate * recipient address based on the current chain and displays transaction status. - * + * * Features: * - Chain-specific recipient addresses * - Transaction status tracking * - Error handling and display * - Transaction hash display - * + * * @example * ```tsx * @@ -40,34 +36,32 @@ export function SendEth() { isPending: isEthTransactionPending, } = useSendTransaction(); - const { - isLoading: isEthTransactionConfirming, - isSuccess: isEthTransactionConfirmed, - } = useWaitForTransactionReceipt({ - hash: ethTransactionHash, - }); + const { isLoading: isEthTransactionConfirming, isSuccess: isEthTransactionConfirmed } = + useWaitForTransactionReceipt({ + hash: ethTransactionHash, + }); // --- Computed Values --- /** * Determines the recipient address based on the current chain. - * + * * Uses different protocol guild addresses for different chains: * - Base: 0x32e3C7fD24e175701A35c224f2238d18439C7dBC * - Other chains: 0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830 - * + * * @returns string - The recipient address for the current chain */ const protocolGuildRecipientAddress = useMemo(() => { // Protocol guild address return chainId === base.id - ? '0x32e3C7fD24e175701A35c224f2238d18439C7dBC' - : '0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830'; + ? "0x32e3C7fD24e175701A35c224f2238d18439C7dBC" + : "0xB3d8d7887693a9852734b4D25e9C0Bb35Ba8a830"; }, [chainId]); // --- Handlers --- /** * Handles sending the ETH transaction. - * + * * This function sends a small amount of ETH (1 wei) to the protocol guild * address for the current chain. The transaction is sent using the wagmi * sendTransaction hook. @@ -94,15 +88,15 @@ export function SendEth() {
Hash: {truncateAddress(ethTransactionHash)}
- Status:{' '} + Status:{" "} {isEthTransactionConfirming - ? 'Confirming...' + ? "Confirming..." : isEthTransactionConfirmed - ? 'Confirmed!' - : 'Pending'} + ? "Confirmed!" + : "Pending"}
)} ); -} +} \ No newline at end of file diff --git a/src/components/ui/wallet/SendSolana.tsx b/src/components/ui/wallet/SendSolana.tsx index 0f69d59..722e524 100644 --- a/src/components/ui/wallet/SendSolana.tsx +++ b/src/components/ui/wallet/SendSolana.tsx @@ -1,32 +1,29 @@ -'use client'; +"use client"; -import { useCallback, useState } from 'react'; -import { - useConnection as useSolanaConnection, - useWallet as useSolanaWallet, -} from '@solana/wallet-adapter-react'; +import { useCallback, useState } from "react"; +import { useConnection as useSolanaConnection, useWallet as useSolanaWallet } from '@solana/wallet-adapter-react'; import { PublicKey, SystemProgram, Transaction } from '@solana/web3.js'; -import { renderError } from '../../../lib/errorUtils'; -import { truncateAddress } from '../../../lib/truncateAddress'; -import { Button } from '../Button'; +import { Button } from "../Button"; +import { truncateAddress } from "../../../lib/truncateAddress"; +import { renderError } from "../../../lib/errorUtils"; /** * SendSolana component handles sending SOL transactions on Solana. - * + * * This component provides a simple interface for users to send SOL transactions * using their connected Solana wallet. It includes transaction status tracking * and error handling. - * + * * Features: * - SOL transaction sending * - Transaction status tracking * - Error handling and display * - Loading state management - * + * * Note: This component is a placeholder implementation. In a real application, * you would integrate with a Solana wallet adapter and transaction library * like @solana/web3.js to handle actual transactions. - * + * * @example * ```tsx * @@ -45,8 +42,7 @@ export function SendSolana() { // This should be replaced but including it from the original demo // https://github.com/farcasterxyz/frames-v2-demo/blob/main/src/components/Demo.tsx#L718 - const ashoatsPhantomSolanaWallet = - 'Ao3gLNZAsbrmnusWVqQCPMrcqNi6jdYgu8T6NCoXXQu1'; + const ashoatsPhantomSolanaWallet = 'Ao3gLNZAsbrmnusWVqQCPMrcqNi6jdYgu8T6NCoXXQu1'; /** * Handles sending the Solana transaction @@ -76,8 +72,7 @@ export function SendSolana() { transaction.recentBlockhash = blockhash; transaction.feePayer = new PublicKey(fromPubkeyStr); - const simulation = - await solanaConnection.simulateTransaction(transaction); + const simulation = await solanaConnection.simulateTransaction(transaction); if (simulation.value.err) { // Gather logs and error details for debugging const logs = simulation.value.logs?.join('\n') ?? 'No logs'; @@ -105,8 +100,7 @@ export function SendSolana() { > Send Transaction (sol) - {solanaTransactionState.status === 'error' && - renderError(solanaTransactionState.error)} + {solanaTransactionState.status === 'error' && renderError(solanaTransactionState.error)} {solanaTransactionState.status === 'success' && (
Hash: {truncateAddress(solanaTransactionState.signature)}
@@ -114,4 +108,4 @@ export function SendSolana() { )} ); -} +} \ No newline at end of file diff --git a/src/components/ui/wallet/SignEvmMessage.tsx b/src/components/ui/wallet/SignEvmMessage.tsx index 82852ef..5880742 100644 --- a/src/components/ui/wallet/SignEvmMessage.tsx +++ b/src/components/ui/wallet/SignEvmMessage.tsx @@ -1,26 +1,26 @@ -'use client'; +"use client"; -import { useCallback } from 'react'; -import { useAccount, useConnect, useSignMessage } from 'wagmi'; -import { base } from 'wagmi/chains'; -import { APP_NAME } from '../../../lib/constants'; -import { renderError } from '../../../lib/errorUtils'; -import { config } from '../../providers/WagmiProvider'; -import { Button } from '../Button'; +import { useCallback } from "react"; +import { useAccount, useConnect, useSignMessage } from "wagmi"; +import { base } from "wagmi/chains"; +import { Button } from "../Button"; +import { config } from "../../providers/WagmiProvider"; +import { APP_NAME } from "../../../lib/constants"; +import { renderError } from "../../../lib/errorUtils"; /** * SignEvmMessage component handles signing messages on EVM-compatible chains. - * + * * This component provides a simple interface for users to sign messages using * their connected EVM wallet. It automatically handles wallet connection if * the user is not already connected, and displays the signature result. - * + * * Features: * - Automatic wallet connection if needed * - Message signing with app name * - Error handling and display * - Signature result display - * + * * @example * ```tsx * @@ -41,12 +41,12 @@ export function SignEvmMessage() { // --- Handlers --- /** * Handles the message signing process. - * + * * This function first ensures the user is connected to an EVM wallet, * then requests them to sign a message containing the app name. * If the user is not connected, it automatically connects using the * Farcaster Frame connector. - * + * * @returns Promise */ const signEvmMessage = useCallback(async () => { @@ -78,4 +78,4 @@ export function SignEvmMessage() { )} ); -} +} \ No newline at end of file diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index a30cb11..0e394ba 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -1,9 +1,9 @@ 'use client'; -import { useCallback, useState } from 'react'; -import { SignIn as SignInCore } from '@farcaster/miniapp-sdk'; -import { useQuickAuth } from '~/hooks/useQuickAuth'; -import { Button } from '../Button'; +import { useCallback, useState } from "react"; +import sdk, { SignIn as SignInCore } from "@farcaster/miniapp-sdk"; +import { Button } from "../Button"; +import { useQuickAuth } from "~/hooks/useQuickAuth"; /** * SignIn component handles Farcaster authentication using QuickAuth. @@ -52,11 +52,11 @@ export function SignIn() { */ const handleSignIn = useCallback(async () => { try { - setAuthState(prev => ({ ...prev, signingIn: true })); + setAuthState((prev) => ({ ...prev, signingIn: true })); setSignInFailure(undefined); - + const success = await signIn(); - + if (!success) { setSignInFailure('Authentication failed'); } @@ -67,7 +67,7 @@ export function SignIn() { } setSignInFailure('Unknown error'); } finally { - setAuthState(prev => ({ ...prev, signingIn: false })); + setAuthState((prev) => ({ ...prev, signingIn: false })); } }, [signIn]); @@ -80,10 +80,10 @@ export function SignIn() { */ const handleSignOut = useCallback(async () => { try { - setAuthState(prev => ({ ...prev, signingOut: true })); + setAuthState((prev) => ({ ...prev, signingOut: true })); await signOut(); } finally { - setAuthState(prev => ({ ...prev, signingOut: false })); + setAuthState((prev) => ({ ...prev, signingOut: false })); } }, [signOut]); @@ -105,9 +105,7 @@ export function SignIn() { {/* Session Information */} {authenticatedUser && (
-
- Authenticated User -
+
Authenticated User
{JSON.stringify(authenticatedUser, null, 2)}
@@ -117,12 +115,8 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && (
-
- Authentication Error -
-
- {signInFailure} -
+
Authentication Error
+
{signInFailure}
)} diff --git a/src/components/ui/wallet/SignSolanaMessage.tsx b/src/components/ui/wallet/SignSolanaMessage.tsx index 96c1339..46c198f 100644 --- a/src/components/ui/wallet/SignSolanaMessage.tsx +++ b/src/components/ui/wallet/SignSolanaMessage.tsx @@ -1,8 +1,8 @@ -'use client'; +"use client"; -import { useCallback, useState } from 'react'; -import { renderError } from '../../../lib/errorUtils'; -import { Button } from '../Button'; +import { useCallback, useState } from "react"; +import { Button } from "../Button"; +import { renderError } from "../../../lib/errorUtils"; interface SignSolanaMessageProps { signMessage?: (message: Uint8Array) => Promise; @@ -10,20 +10,20 @@ interface SignSolanaMessageProps { /** * SignSolanaMessage component handles signing messages on Solana. - * + * * This component provides a simple interface for users to sign messages using * their connected Solana wallet. It accepts a signMessage function as a prop * and handles the complete signing flow including error handling. - * + * * Features: * - Message signing with Solana wallet * - Error handling and display * - Signature result display (base64 encoded) * - Loading state management - * + * * @param props - Component props * @param props.signMessage - Function to sign messages with Solana wallet - * + * * @example * ```tsx * @@ -38,11 +38,11 @@ export function SignSolanaMessage({ signMessage }: SignSolanaMessageProps) { // --- Handlers --- /** * Handles the Solana message signing process. - * + * * This function encodes a message as UTF-8 bytes, signs it using the provided * signMessage function, and displays the base64-encoded signature result. * It includes comprehensive error handling and loading state management. - * + * * @returns Promise */ const handleSignMessage = useCallback(async () => { @@ -51,7 +51,7 @@ export function SignSolanaMessage({ signMessage }: SignSolanaMessageProps) { if (!signMessage) { throw new Error('no Solana signMessage'); } - const input = new TextEncoder().encode('Hello from Solana!'); + const input = new TextEncoder().encode("Hello from Solana!"); const signatureBytes = await signMessage(input); const signature = btoa(String.fromCharCode(...signatureBytes)); setSignature(signature); @@ -84,4 +84,4 @@ export function SignSolanaMessage({ signMessage }: SignSolanaMessageProps) { )} ); -} +} \ No newline at end of file diff --git a/src/components/ui/wallet/index.ts b/src/components/ui/wallet/index.ts index a513747..1cacbd2 100644 --- a/src/components/ui/wallet/index.ts +++ b/src/components/ui/wallet/index.ts @@ -2,4 +2,4 @@ export { SignIn } from './SignIn'; export { SignEvmMessage } from './SignEvmMessage'; export { SendEth } from './SendEth'; export { SignSolanaMessage } from './SignSolanaMessage'; -export { SendSolana } from './SendSolana'; +export { SendSolana } from './SendSolana'; \ No newline at end of file diff --git a/src/hooks/useDetectClickOutside.ts b/src/hooks/useDetectClickOutside.ts index c1d4cf6..e6b1533 100644 --- a/src/hooks/useDetectClickOutside.ts +++ b/src/hooks/useDetectClickOutside.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; export function useDetectClickOutside( ref: React.RefObject, - callback: () => void, + callback: () => void ) { useEffect(() => { function handleClickOutside(event: MouseEvent) { diff --git a/src/hooks/useNeynarUser.ts b/src/hooks/useNeynarUser.ts index a5b1101..e89e569 100644 --- a/src/hooks/useNeynarUser.ts +++ b/src/hooks/useNeynarUser.ts @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; export interface NeynarUser { fid: number; @@ -19,21 +19,20 @@ export function useNeynarUser(context?: { user?: { fid?: number } }) { setLoading(true); setError(null); fetch(`/api/users?fids=${context.user.fid}`) - .then(response => { - if (!response.ok) - throw new Error(`HTTP error! status: ${response.status}`); + .then((response) => { + if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`); return response.json(); }) - .then(data => { + .then((data) => { if (data.users?.[0]) { setUser(data.users[0]); } else { setUser(null); } }) - .catch(err => setError(err.message)) + .catch((err) => setError(err.message)) .finally(() => setLoading(false)); }, [context?.user?.fid]); return { user, loading, error }; -} +} \ No newline at end of file diff --git a/src/hooks/useQuickAuth.ts b/src/hooks/useQuickAuth.ts index ec1a088..f14cac5 100644 --- a/src/hooks/useQuickAuth.ts +++ b/src/hooks/useQuickAuth.ts @@ -34,25 +34,25 @@ interface UseQuickAuthReturn { /** * Custom hook for managing QuickAuth authentication state - * + * * This hook provides a complete authentication flow using Farcaster's QuickAuth: * - Automatically checks for existing authentication on mount * - Validates tokens with the server-side API * - Manages authentication state in memory (no persistence) * - Provides sign-in/sign-out functionality - * + * * QuickAuth tokens are managed in memory only, so signing out of the Farcaster * client will automatically sign the user out of this mini app as well. - * + * * @returns {UseQuickAuthReturn} Object containing user state and authentication methods - * + * * @example * ```tsx * const { authenticatedUser, status, signIn, signOut } = useQuickAuth(); - * + * * if (status === 'loading') return
Loading...
; * if (status === 'unauthenticated') return ; - * + * * return ( *
*

Welcome, FID: {authenticatedUser?.fid}

@@ -63,20 +63,17 @@ interface UseQuickAuthReturn { */ export function useQuickAuth(): UseQuickAuthReturn { // Current authenticated user data - const [authenticatedUser, setAuthenticatedUser] = - useState(null); + const [authenticatedUser, setAuthenticatedUser] = useState(null); // Current authentication status const [status, setStatus] = useState('loading'); /** * Validates a QuickAuth token with the server-side API - * + * * @param {string} authToken - The JWT token to validate * @returns {Promise} User data if valid, null otherwise */ - const validateTokenWithServer = async ( - authToken: string, - ): Promise => { + const validateTokenWithServer = async (authToken: string): Promise => { try { const validationResponse = await fetch('/api/auth/validate', { method: 'POST', @@ -88,7 +85,7 @@ export function useQuickAuth(): UseQuickAuthReturn { const responseData = await validationResponse.json(); return responseData.user; } - + return null; } catch (error) { console.error('Token validation failed:', error); @@ -105,11 +102,11 @@ export function useQuickAuth(): UseQuickAuthReturn { try { // Attempt to retrieve existing token from QuickAuth SDK const { token } = await sdk.quickAuth.getToken(); - + if (token) { // Validate the token with our server-side API const validatedUserSession = await validateTokenWithServer(token); - + if (validatedUserSession) { // Token is valid, set authenticated state setAuthenticatedUser(validatedUserSession); @@ -133,24 +130,24 @@ export function useQuickAuth(): UseQuickAuthReturn { /** * Initiates the QuickAuth sign-in process - * + * * Uses sdk.quickAuth.getToken() to get a QuickAuth session token. * If there is already a session token in memory that hasn't expired, * it will be immediately returned, otherwise a fresh one will be acquired. - * + * * @returns {Promise} True if sign-in was successful, false otherwise */ const signIn = useCallback(async (): Promise => { try { setStatus('loading'); - + // Get QuickAuth session token const { token } = await sdk.quickAuth.getToken(); - + if (token) { // Validate the token with our server-side API const validatedUserSession = await validateTokenWithServer(token); - + if (validatedUserSession) { // Authentication successful, update user state setAuthenticatedUser(validatedUserSession); @@ -158,7 +155,7 @@ export function useQuickAuth(): UseQuickAuthReturn { return true; } } - + // Authentication failed, clear user state setStatus('unauthenticated'); return false; @@ -171,7 +168,7 @@ export function useQuickAuth(): UseQuickAuthReturn { /** * Signs out the current user and clears the authentication state - * + * * Since QuickAuth tokens are managed in memory only, this simply clears * the local user state. The actual token will be cleared when the * user signs out of their Farcaster client. @@ -184,7 +181,7 @@ export function useQuickAuth(): UseQuickAuthReturn { /** * Retrieves the current authentication token from QuickAuth - * + * * @returns {Promise} The current auth token, or null if not authenticated */ const getToken = useCallback(async (): Promise => { @@ -204,4 +201,4 @@ export function useQuickAuth(): UseQuickAuthReturn { signOut, getToken, }; -} +} \ No newline at end of file diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 39a4c7f..dc05838 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -65,15 +65,14 @@ export const APP_SPLASH_URL: string = `${APP_URL}/splash.png`; * Background color for the splash screen. * Used as fallback when splash image is loading. */ -export const APP_SPLASH_BACKGROUND_COLOR: string = '#f7f7f7'; +export const APP_SPLASH_BACKGROUND_COLOR: string = "#f7f7f7"; /** * Account association for the mini app. * Used to associate the mini app with a Farcaster account. * If not provided, the mini app will be unsigned and have limited capabilities. */ -export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = - undefined; +export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = undefined; // --- UI Configuration --- /** @@ -90,8 +89,7 @@ export const APP_BUTTON_TEXT: string = 'Launch NSK'; * Neynar webhook endpoint. Otherwise, falls back to a local webhook * endpoint for development and testing. */ -export const APP_WEBHOOK_URL: string = - process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID +export const APP_WEBHOOK_URL: string = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `${APP_URL}/api/webhook`; diff --git a/src/lib/errorUtils.tsx b/src/lib/errorUtils.tsx index 7d06d8e..c42b070 100644 --- a/src/lib/errorUtils.tsx +++ b/src/lib/errorUtils.tsx @@ -1,9 +1,9 @@ -import { type ReactElement } from 'react'; -import { BaseError, UserRejectedRequestError } from 'viem'; +import { type ReactElement } from "react"; +import { BaseError, UserRejectedRequestError } from "viem"; /** * Renders an error object in a user-friendly format. - * + * * This utility function takes an error object and renders it as a React element * with consistent styling. It handles different types of errors including: * - Error objects with message properties @@ -11,14 +11,14 @@ import { BaseError, UserRejectedRequestError } from 'viem'; * - String errors * - Unknown error types * - User rejection errors (special handling for wallet rejections) - * + * * The rendered error is displayed in a gray container with monospace font * for better readability of technical error details. User rejections are * displayed with a simpler, more user-friendly message. - * + * * @param error - The error object to render * @returns ReactElement - A styled error display component, or null if no error - * + * * @example * ```tsx * {isError && renderError(error)} @@ -27,11 +27,11 @@ import { BaseError, UserRejectedRequestError } from 'viem'; export function renderError(error: unknown): ReactElement | null { // Handle null/undefined errors if (!error) return null; - + // Special handling for user rejections in wallet operations if (error instanceof BaseError) { const isUserRejection = error.walk( - e => e instanceof UserRejectedRequestError, + (e) => e instanceof UserRejectedRequestError ); if (isUserRejection) { @@ -43,10 +43,10 @@ export function renderError(error: unknown): ReactElement | null { ); } } - + // Extract error message from different error types let errorMessage: string; - + if (error instanceof Error) { errorMessage = error.message; } else if (typeof error === 'object' && error !== null && 'error' in error) { @@ -63,4 +63,4 @@ export function renderError(error: unknown): ReactElement | null {
{errorMessage}
); -} +} \ No newline at end of file diff --git a/src/lib/kv.ts b/src/lib/kv.ts index 2963f90..eefc680 100644 --- a/src/lib/kv.ts +++ b/src/lib/kv.ts @@ -1,25 +1,23 @@ -import { FrameNotificationDetails } from '@farcaster/miniapp-sdk'; -import { Redis } from '@upstash/redis'; -import { APP_NAME } from './constants'; +import { FrameNotificationDetails } from "@farcaster/miniapp-sdk"; +import { Redis } from "@upstash/redis"; +import { APP_NAME } from "./constants"; // In-memory fallback storage const localStore = new Map(); // Use Redis if KV env vars are present, otherwise use in-memory const useRedis = process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN; -const redis = useRedis - ? new Redis({ - url: process.env.KV_REST_API_URL!, - token: process.env.KV_REST_API_TOKEN!, - }) - : null; +const redis = useRedis ? new Redis({ + url: process.env.KV_REST_API_URL!, + token: process.env.KV_REST_API_TOKEN!, +}) : null; function getUserNotificationDetailsKey(fid: number): string { return `${APP_NAME}:user:${fid}`; } export async function getUserNotificationDetails( - fid: number, + fid: number ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { @@ -30,7 +28,7 @@ export async function getUserNotificationDetails( export async function setUserNotificationDetails( fid: number, - notificationDetails: FrameNotificationDetails, + notificationDetails: FrameNotificationDetails ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { @@ -41,7 +39,7 @@ export async function setUserNotificationDetails( } export async function deleteUserNotificationDetails( - fid: number, + fid: number ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { diff --git a/src/lib/neynar.ts b/src/lib/neynar.ts index 0d8f663..f0b6e23 100644 --- a/src/lib/neynar.ts +++ b/src/lib/neynar.ts @@ -1,15 +1,11 @@ -import { - NeynarAPIClient, - Configuration, - WebhookUserCreated, -} from '@neynar/nodejs-sdk'; +import { NeynarAPIClient, Configuration, WebhookUserCreated } from '@neynar/nodejs-sdk'; import { APP_URL } from './constants'; let neynarClient: NeynarAPIClient | null = null; // Example usage: // const client = getNeynarClient(); -// const user = await client.lookupUserByFid(fid); +// const user = await client.lookupUserByFid(fid); export function getNeynarClient() { if (!neynarClient) { const apiKey = process.env.NEYNAR_API_KEY; @@ -37,12 +33,12 @@ export async function getNeynarUser(fid: number): Promise { type SendMiniAppNotificationResult = | { - state: 'error'; + state: "error"; error: unknown; } - | { state: 'no_token' } - | { state: 'rate_limit' } - | { state: 'success' }; + | { state: "no_token" } + | { state: "rate_limit" } + | { state: "success" }; export async function sendNeynarMiniAppNotification({ fid, @@ -62,19 +58,19 @@ export async function sendNeynarMiniAppNotification({ target_url: APP_URL, }; - const result = await client.publishFrameNotifications({ - targetFids, - notification, + const result = await client.publishFrameNotifications({ + targetFids, + notification }); if (result.notification_deliveries.length > 0) { - return { state: 'success' }; + return { state: "success" }; } else if (result.notification_deliveries.length === 0) { - return { state: 'no_token' }; + return { state: "no_token" }; } else { - return { state: 'error', error: result || 'Unknown error' }; + return { state: "error", error: result || "Unknown error" }; } } catch (error) { - return { state: 'error', error }; + return { state: "error", error }; } -} +} \ No newline at end of file diff --git a/src/lib/notifs.ts b/src/lib/notifs.ts index 995d54f..72007b1 100644 --- a/src/lib/notifs.ts +++ b/src/lib/notifs.ts @@ -1,18 +1,18 @@ import { SendNotificationRequest, sendNotificationResponseSchema, -} from '@farcaster/miniapp-sdk'; -import { getUserNotificationDetails } from '~/lib/kv'; -import { APP_URL } from './constants'; +} from "@farcaster/miniapp-sdk"; +import { getUserNotificationDetails } from "~/lib/kv"; +import { APP_URL } from "./constants"; type SendMiniAppNotificationResult = | { - state: 'error'; + state: "error"; error: unknown; } - | { state: 'no_token' } - | { state: 'rate_limit' } - | { state: 'success' }; + | { state: "no_token" } + | { state: "rate_limit" } + | { state: "success" }; export async function sendMiniAppNotification({ fid, @@ -25,13 +25,13 @@ export async function sendMiniAppNotification({ }): Promise { const notificationDetails = await getUserNotificationDetails(fid); if (!notificationDetails) { - return { state: 'no_token' }; + return { state: "no_token" }; } const response = await fetch(notificationDetails.url, { - method: 'POST', + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ notificationId: crypto.randomUUID(), @@ -48,17 +48,17 @@ export async function sendMiniAppNotification({ const responseBody = sendNotificationResponseSchema.safeParse(responseJson); if (responseBody.success === false) { // Malformed response - return { state: 'error', error: responseBody.error.errors }; + return { state: "error", error: responseBody.error.errors }; } if (responseBody.data.result.rateLimitedTokens.length) { // Rate limited - return { state: 'rate_limit' }; + return { state: "rate_limit" }; } - return { state: 'success' }; + return { state: "success" }; } else { // Error response - return { state: 'error', error: responseJson }; + return { state: "error", error: responseJson }; } } diff --git a/src/lib/truncateAddress.ts b/src/lib/truncateAddress.ts index c5e16ef..1f6c3f0 100644 --- a/src/lib/truncateAddress.ts +++ b/src/lib/truncateAddress.ts @@ -1,4 +1,4 @@ export const truncateAddress = (address: string) => { - if (!address) return ''; + if (!address) return ""; return `${address.slice(0, 14)}...${address.slice(-12)}`; }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 547bb28..59430e8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type Manifest } from '@farcaster/miniapp-node'; import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; +import { type Manifest } from '@farcaster/miniapp-node'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, @@ -10,8 +10,7 @@ import { APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_SPLASH_URL, - APP_TAGS, - APP_URL, + APP_TAGS, APP_URL, APP_WEBHOOK_URL, APP_ACCOUNT_ASSOCIATION, } from './constants'; @@ -22,12 +21,12 @@ export function cn(...inputs: ClassValue[]) { export function getMiniAppEmbedMetadata(ogImageUrl?: string) { return { - version: 'next', + version: "next", imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL, button: { title: APP_BUTTON_TEXT, action: { - type: 'launch_frame', + type: "launch_frame", name: APP_NAME, url: APP_URL, splashImageUrl: APP_SPLASH_URL, @@ -45,12 +44,12 @@ export async function getFarcasterDomainManifest(): Promise { return { accountAssociation: APP_ACCOUNT_ASSOCIATION, miniapp: { - version: '1', - name: APP_NAME ?? 'Neynar Starter Kit', + version: "1", + name: APP_NAME ?? "Neynar Starter Kit", iconUrl: APP_ICON_URL, homeUrl: APP_URL, imageUrl: APP_OG_IMAGE_URL, - buttonTitle: APP_BUTTON_TEXT ?? 'Launch Mini App', + buttonTitle: APP_BUTTON_TEXT ?? "Launch Mini App", splashImageUrl: APP_SPLASH_URL, splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR, webhookUrl: APP_WEBHOOK_URL, diff --git a/tailwind.config.ts b/tailwind.config.ts new file mode 100644 index 0000000..7583b61 --- /dev/null +++ b/tailwind.config.ts @@ -0,0 +1,60 @@ +import type { Config } from "tailwindcss"; + +/** + * Tailwind CSS Configuration + * + * This configuration centralizes all theme colors for the mini app. + * To change the app's color scheme, simply update the 'primary' color value below. + * + * Example theme changes: + * - Blue theme: primary: "#3182CE" + * - Green theme: primary: "#059669" + * - Red theme: primary: "#DC2626" + * - Orange theme: primary: "#EA580C" + */ +export default { + darkMode: "media", + content: [ + "./src/pages/**/*.{js,ts,jsx,tsx,mdx}", + "./src/components/**/*.{js,ts,jsx,tsx,mdx}", + "./src/app/**/*.{js,ts,jsx,tsx,mdx}", + ], + theme: { + extend: { + colors: { + // Main theme color - change this to update the entire app's color scheme + primary: "#8b5cf6", // Main brand color + "primary-light": "#a78bfa", // For hover states + "primary-dark": "#7c3aed", // For active states + + // Secondary colors for backgrounds and text + secondary: "#f8fafc", // Light backgrounds + "secondary-dark": "#334155", // Dark backgrounds + + // Legacy CSS variables for backward compatibility + background: 'var(--background)', + foreground: 'var(--foreground)' + }, + borderRadius: { + lg: 'var(--radius)', + md: 'calc(var(--radius) - 2px)', + sm: 'calc(var(--radius) - 4px)' + }, + // Custom spacing for consistent layout + spacing: { + '18': '4.5rem', + '88': '22rem', + }, + // Custom container sizes + maxWidth: { + 'xs': '20rem', + 'sm': '24rem', + 'md': '28rem', + 'lg': '32rem', + 'xl': '36rem', + '2xl': '42rem', + } + } + }, + plugins: [require("tailwindcss-animate")], +} satisfies Config; diff --git a/vercel.json b/vercel.json index 87b9de5..a4d94e7 100644 --- a/vercel.json +++ b/vercel.json @@ -1,4 +1,4 @@ { "buildCommand": "next build", "framework": "nextjs" -} +} \ No newline at end of file From 5fd0e215322f6ffcfb1c502f22e57cd0ed625177 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 16 Jul 2025 17:28:36 +0530 Subject: [PATCH 3/8] Revert "fix: SIWN dependencies" This reverts commit 4ba948083278067334670b0573b8029755f4f7e3. --- bin/init.js | 9 - package.json | 2 +- src/components/ui/NeynarAuthButton/index.tsx | 202 +++++++++++++++++-- 3 files changed, 182 insertions(+), 31 deletions(-) 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() { ) : ( )} - {status === 'authenticated' && ( + {status === 'authenticated' && session?.provider === 'farcaster' && ( )} {/* Session Information */} - {authenticatedUser && ( + {session && (
-
Authenticated User
+
Session
- {JSON.stringify(authenticatedUser, null, 2)} + {JSON.stringify(session, null, 2)}
)} @@ -115,10 +142,20 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && (
-
Authentication Error
+
SIWF Result
{signInFailure}
)} + + {/* Success Result Display */} + {signInResult && !authState.signingIn && ( +
+
SIWF Result
+
+ {JSON.stringify(signInResult, null, 2)} +
+
+ )} ); } diff --git a/src/hooks/useQuickAuth.ts b/src/hooks/useQuickAuth.ts deleted file mode 100644 index f14cac5..0000000 --- a/src/hooks/useQuickAuth.ts +++ /dev/null @@ -1,204 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback } from 'react'; -import { sdk } from '@farcaster/miniapp-sdk'; - -/** - * Represents the current authenticated user state - */ -interface AuthenticatedUser { - /** The user's Farcaster ID (FID) */ - fid: number; -} - -/** - * Possible authentication states for QuickAuth - */ -type QuickAuthStatus = 'loading' | 'authenticated' | 'unauthenticated'; - -/** - * Return type for the useQuickAuth hook - */ -interface UseQuickAuthReturn { - /** Current authenticated user data, or null if not authenticated */ - authenticatedUser: AuthenticatedUser | null; - /** Current authentication status */ - status: QuickAuthStatus; - /** Function to initiate the sign-in process using QuickAuth */ - signIn: () => Promise; - /** Function to sign out and clear the current authentication state */ - signOut: () => Promise; - /** Function to retrieve the current authentication token */ - getToken: () => Promise; -} - -/** - * Custom hook for managing QuickAuth authentication state - * - * This hook provides a complete authentication flow using Farcaster's QuickAuth: - * - Automatically checks for existing authentication on mount - * - Validates tokens with the server-side API - * - Manages authentication state in memory (no persistence) - * - Provides sign-in/sign-out functionality - * - * QuickAuth tokens are managed in memory only, so signing out of the Farcaster - * client will automatically sign the user out of this mini app as well. - * - * @returns {UseQuickAuthReturn} Object containing user state and authentication methods - * - * @example - * ```tsx - * const { authenticatedUser, status, signIn, signOut } = useQuickAuth(); - * - * if (status === 'loading') return
Loading...
; - * if (status === 'unauthenticated') return ; - * - * return ( - *
- *

Welcome, FID: {authenticatedUser?.fid}

- * - *
- * ); - * ``` - */ -export function useQuickAuth(): UseQuickAuthReturn { - // Current authenticated user data - const [authenticatedUser, setAuthenticatedUser] = useState(null); - // Current authentication status - const [status, setStatus] = useState('loading'); - - /** - * Validates a QuickAuth token with the server-side API - * - * @param {string} authToken - The JWT token to validate - * @returns {Promise} User data if valid, null otherwise - */ - const validateTokenWithServer = async (authToken: string): Promise => { - try { - const validationResponse = await fetch('/api/auth/validate', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ token: authToken }), - }); - - if (validationResponse.ok) { - const responseData = await validationResponse.json(); - return responseData.user; - } - - return null; - } catch (error) { - console.error('Token validation failed:', error); - return null; - } - }; - - /** - * Checks for existing authentication token and validates it on component mount - * This runs automatically when the hook is first used - */ - useEffect(() => { - const checkExistingAuthentication = async () => { - try { - // Attempt to retrieve existing token from QuickAuth SDK - const { token } = await sdk.quickAuth.getToken(); - - if (token) { - // Validate the token with our server-side API - const validatedUserSession = await validateTokenWithServer(token); - - if (validatedUserSession) { - // Token is valid, set authenticated state - setAuthenticatedUser(validatedUserSession); - setStatus('authenticated'); - } else { - // Token is invalid or expired, clear authentication state - setStatus('unauthenticated'); - } - } else { - // No existing token found, user is not authenticated - setStatus('unauthenticated'); - } - } catch (error) { - console.error('Error checking existing authentication:', error); - setStatus('unauthenticated'); - } - }; - - checkExistingAuthentication(); - }, []); - - /** - * Initiates the QuickAuth sign-in process - * - * Uses sdk.quickAuth.getToken() to get a QuickAuth session token. - * If there is already a session token in memory that hasn't expired, - * it will be immediately returned, otherwise a fresh one will be acquired. - * - * @returns {Promise} True if sign-in was successful, false otherwise - */ - const signIn = useCallback(async (): Promise => { - try { - setStatus('loading'); - - // Get QuickAuth session token - const { token } = await sdk.quickAuth.getToken(); - - if (token) { - // Validate the token with our server-side API - const validatedUserSession = await validateTokenWithServer(token); - - if (validatedUserSession) { - // Authentication successful, update user state - setAuthenticatedUser(validatedUserSession); - setStatus('authenticated'); - return true; - } - } - - // Authentication failed, clear user state - setStatus('unauthenticated'); - return false; - } catch (error) { - console.error('Sign-in process failed:', error); - setStatus('unauthenticated'); - return false; - } - }, []); - - /** - * Signs out the current user and clears the authentication state - * - * Since QuickAuth tokens are managed in memory only, this simply clears - * the local user state. The actual token will be cleared when the - * user signs out of their Farcaster client. - */ - const signOut = useCallback(async (): Promise => { - // Clear local user state - setAuthenticatedUser(null); - setStatus('unauthenticated'); - }, []); - - /** - * Retrieves the current authentication token from QuickAuth - * - * @returns {Promise} The current auth token, or null if not authenticated - */ - const getToken = useCallback(async (): Promise => { - try { - const { token } = await sdk.quickAuth.getToken(); - return token; - } catch (error) { - console.error('Failed to retrieve authentication token:', error); - return null; - } - }, []); - - return { - authenticatedUser, - status, - signIn, - signOut, - getToken, - }; -} \ No newline at end of file From 196378daebfe5d69c6182c72708e37a36a9afc6d Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 16 Jul 2025 17:33:02 +0530 Subject: [PATCH 7/8] Revert "format" This reverts commit 5fbd9c5c096a278a3b52df169e3903a4da783239. --- scripts/deploy.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/deploy.ts b/scripts/deploy.ts index e64d4d2..cfc629d 100755 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -890,4 +890,4 @@ async function main(): Promise { } } -main(); +main(); \ No newline at end of file From bf563154ca9ffd5551828d1ab4798377f9616bf8 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 16 Jul 2025 17:50:03 +0530 Subject: [PATCH 8/8] Fix merge conflict issues --- src/components/ui/NeynarAuthButton/index.tsx | 166 ++----------------- src/components/ui/tabs/ActionsTab.tsx | 40 ++--- 2 files changed, 36 insertions(+), 170 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 0eb1605..33e9a2d 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -5,8 +5,8 @@ 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 { 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 { @@ -141,7 +141,7 @@ export function NeynarAuthButton() { const updateSessionWithSigners = useCallback( async ( signers: StoredAuthState['signers'], - user: StoredAuthState['user'], + user: StoredAuthState['user'] ) => { if (!useBackendFlow) return; @@ -308,7 +308,7 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + try { const response = await fetch( `/api/auth/signer?signerUuid=${signerUuid}` @@ -321,7 +321,7 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + // Increment retry count for other errors retryCount++; if (retryCount >= maxRetries) { @@ -329,7 +329,7 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + throw new Error(`Failed to poll signer status: ${response.status}`); } @@ -384,146 +384,7 @@ export function NeynarAuthButton() { generateNonce(); }, []); - // 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); - } - } - }, [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, 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]); - - // 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) + // Load stored auth state on mount (only for frontend flow) useEffect(() => { if (!useBackendFlow) { const stored = getItem(STORAGE_KEY); @@ -582,7 +443,7 @@ export function NeynarAuthButton() { useEffect(() => { 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; @@ -598,9 +459,14 @@ export function NeynarAuthButton() { // Handle fetching signers after successful authentication useEffect(() => { - if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) { + if ( + message && + signature && + !isSignerFlowRunning && + !signerFlowStartedRef.current + ) { signerFlowStartedRef.current = true; - + const handleSignerFlow = async () => { setIsSignerFlowRunning(true); try { @@ -618,7 +484,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 @@ -743,7 +609,7 @@ export function NeynarAuthButton() { clearInterval(pollingInterval); setPollingInterval(null); } - + // Reset signer flow flag signerFlowStartedRef.current = false; } catch (error) { diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 4c345cc..cc072d1 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useCallback, useState } from "react"; -import { useMiniApp } from "@neynar/react"; -import { ShareButton } from "../Share"; -import { Button } from "../Button"; -import { SignIn } from "../wallet/SignIn"; -import { type Haptics } from "@farcaster/miniapp-sdk"; -import { APP_URL } from "~/lib/constants"; +import { useCallback, useState } from 'react'; +import { useMiniApp } from '@neynar/react'; +import { ShareButton } from '../Share'; +import { Button } from '../Button'; +import { SignIn } from '../wallet/SignIn'; +import { type Haptics } from '@farcaster/miniapp-sdk'; +import { APP_URL } from '~/lib/constants'; import { NeynarAuthButton } from '../NeynarAuthButton/index'; /** @@ -124,16 +124,16 @@ export function ActionsTab() { // --- Render --- return ( -
+
{/* Share functionality */} {/* Authentication */} @@ -147,25 +147,25 @@ export function ActionsTab() { onClick={() => actions.openUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ') } - className='w-full' + className="w-full" > Open Link - {/* Notification functionality */} {notificationState.sendStatus && ( -
+
Send notification result: {notificationState.sendStatus}
)} @@ -174,14 +174,14 @@ export function ActionsTab() { {/* Haptic feedback controls */} -
-