fix: SIWN dependencies

This commit is contained in:
veganbeef 2025-07-15 09:25:08 -07:00
parent 16c433a13c
commit 4ba9480832
No known key found for this signature in database
4 changed files with 229 additions and 242 deletions

View File

@ -455,7 +455,6 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
// Add dependencies // Add dependencies
packageJson.dependencies = { packageJson.dependencies = {
'@farcaster/auth-client': '>=0.3.0 <1.0.0', '@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-node': '>=0.1.5 <1.0.0',
'@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0', '@farcaster/miniapp-sdk': '>=0.1.6 <1.0.0',
'@farcaster/miniapp-wagmi-connector': '^1.0.0', '@farcaster/miniapp-wagmi-connector': '^1.0.0',
@ -482,6 +481,12 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
siwe: '^3.0.0', siwe: '^3.0.0',
}; };
// Add auth-kit and quick-auth dependencies if useSponsoredSigner is true
if (answers.useSponsoredSigner) {
packageJson.dependencies['@farcaster/auth-kit'] = '>=0.6.0 <1.0.0';
packageJson.dependencies['next-auth'] = '^4.24.11';
}
packageJson.devDependencies = { packageJson.devDependencies = {
"@types/inquirer": "^9.0.8", "@types/inquirer": "^9.0.8",
"@types/node": "^20", "@types/node": "^20",
@ -692,6 +697,15 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.rmSync(binPath, { recursive: true, force: true }); 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 // Initialize git repository
console.log('\nInitializing git repository...'); console.log('\nInitializing git repository...');
execSync('git init', { cwd: projectPath }); execSync('git init', { cwd: projectPath });

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.7.1", "version": "1.7.2",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",

View File

@ -1,16 +1,20 @@
'use client'; 'use client';
import '@farcaster/auth-kit/styles.css'; import '@farcaster/auth-kit/styles.css';
import { useSignIn, UseSignInData } from '@farcaster/auth-kit'; import { useSignIn } from '@farcaster/auth-kit';
import { useCallback, useEffect, useState, useRef } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { cn } from '~/lib/utils'; import { cn } from '~/lib/utils';
import { Button } from '~/components/ui/Button'; import { Button } from '~/components/ui/Button';
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton'; import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog'; import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
import { getItem, removeItem, setItem } from '~/lib/localStorage'; import { getItem, removeItem, setItem } from '~/lib/localStorage';
import { useMiniApp } from '@neynar/react'; import { useMiniApp } from '@neynar/react';
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; import {
import { useQuickAuth } from '~/hooks/useQuickAuth'; signIn as backendSignIn,
signOut as backendSignOut,
useSession,
} from 'next-auth/react';
import sdk, { SignIn as SignInCore } from '@farcaster/miniapp-sdk';
type User = { type User = {
fid: number; fid: number;
@ -94,8 +98,7 @@ export function NeynarAuthButton() {
const [storedAuth, setStoredAuth] = useState<StoredAuthState | null>(null); const [storedAuth, setStoredAuth] = useState<StoredAuthState | null>(null);
const [signersLoading, setSignersLoading] = useState(false); const [signersLoading, setSignersLoading] = useState(false);
const { context } = useMiniApp(); const { context } = useMiniApp();
const { authenticatedUser: quickAuthUser, signIn: quickAuthSignIn, signOut: quickAuthSignOut } = useQuickAuth(); const { data: session } = useSession();
// New state for unified dialog flow // New state for unified dialog flow
const [showDialog, setShowDialog] = useState(false); const [showDialog, setShowDialog] = useState(false);
const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>(
@ -109,74 +112,10 @@ export function NeynarAuthButton() {
); );
const [message, setMessage] = useState<string | null>(null); const [message, setMessage] = useState<string | null>(null);
const [signature, setSignature] = useState<string | null>(null); const [signature, setSignature] = useState<string | null>(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 // 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; const useBackendFlow = context !== undefined;
// Helper function to fetch user data from Neynar API
const fetchUserData = useCallback(
async (fid: number): Promise<User | null> => {
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<StoredAuthState>(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<StoredAuthState>(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 // Helper function to create a signer
const createSigner = useCallback(async () => { const createSigner = useCallback(async () => {
try { try {
@ -205,15 +144,43 @@ export function NeynarAuthButton() {
if (!useBackendFlow) return; if (!useBackendFlow) return;
try { try {
// For backend flow, use QuickAuth to sign in // For backend flow, we need to sign in again with the additional data
if (signers && signers.length > 0) { if (message && signature) {
await quickAuthSignIn(); const signInData = {
message,
signature,
redirect: false,
nonce: nonce || '',
fid: user?.fid?.toString() || '',
signers: JSON.stringify(signers),
user: JSON.stringify(user),
};
await backendSignIn('neynar', signInData);
} }
} catch (error) { } catch (error) {
console.error('❌ Error updating session with signers:', error); console.error('❌ Error updating session with signers:', error);
} }
}, },
[useBackendFlow, quickAuthSignIn] [useBackendFlow, message, signature, nonce]
);
// Helper function to fetch user data from Neynar API
const fetchUserData = useCallback(
async (fid: number): Promise<User | null> => {
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 // Helper function to generate signed key request
@ -277,11 +244,9 @@ export function NeynarAuthButton() {
if (useBackendFlow) { if (useBackendFlow) {
// For backend flow, update session with signers // For backend flow, update session with signers
if (signerData.signers && signerData.signers.length > 0) { if (signerData.signers && signerData.signers.length > 0) {
// Get user data for the first signer const user =
let user: StoredAuthState['user'] | null = null; signerData.user ||
if (signerData.signers[0].fid) { (await fetchUserData(signerData.signers[0].fid));
user = await fetchUserData(signerData.signers[0].fid) as StoredAuthState['user'];
}
await updateSessionWithSigners(signerData.signers, user); await updateSessionWithSigners(signerData.signers, user);
} }
return signerData.signers; return signerData.signers;
@ -324,46 +289,14 @@ export function NeynarAuthButton() {
// Helper function to poll signer status // Helper function to poll signer status
const startPolling = useCallback( const startPolling = useCallback(
(signerUuid: string, message: string, signature: string) => { (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 () => { const interval = setInterval(async () => {
// Check if we've been polling too long
if (Date.now() - startTime > maxPollingTime) {
clearInterval(interval);
setPollingInterval(null);
return;
}
try { try {
const response = await fetch( const response = await fetch(
`/api/auth/signer?signerUuid=${signerUuid}` `/api/auth/signer?signerUuid=${signerUuid}`
); );
if (!response.ok) { if (!response.ok) {
// Check if it's a rate limit error throw new Error('Failed to poll signer status');
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(); const signerData = await response.json();
@ -385,7 +318,7 @@ export function NeynarAuthButton() {
setPollingInterval(interval); setPollingInterval(interval);
}, },
[fetchAllSigners, pollingInterval] [fetchAllSigners]
); );
// Cleanup polling on unmount // Cleanup polling on unmount
@ -394,7 +327,6 @@ export function NeynarAuthButton() {
if (pollingInterval) { if (pollingInterval) {
clearInterval(pollingInterval); clearInterval(pollingInterval);
} }
signerFlowStartedRef.current = false;
}; };
}, [pollingInterval]); }, [pollingInterval]);
@ -427,124 +359,68 @@ export function NeynarAuthButton() {
} }
}, [useBackendFlow]); }, [useBackendFlow]);
// Auth Kit data synchronization (only for browser flow) // Success callback - this is critical!
useEffect(() => { const onSuccessCallback = useCallback(
if (!useBackendFlow) { async (res: unknown) => {
setMessage(data?.message || null); if (!useBackendFlow) {
setSignature(data?.signature || null); // Only handle localStorage for frontend flow
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
// Reset the signer flow flag when message/signature change const user = await fetchUserData(res.fid);
if (data?.message && data?.signature) { const authState: StoredAuthState = {
signerFlowStartedRef.current = false; ...existingAuth,
isAuthenticated: true,
user: user as StoredAuthState['user'],
signers: existingAuth?.signers || [], // Preserve existing signers
};
setItem<StoredAuthState>(STORAGE_KEY, authState);
setStoredAuth(authState);
} }
} // For backend flow, the session will be handled by NextAuth
}, [useBackendFlow, data?.message, data?.signature]); },
[useBackendFlow, fetchUserData]
);
// Connect for frontend flow when nonce is available (only for browser flow) // Error callback
const onErrorCallback = useCallback((error?: Error | null) => {
console.error('❌ Sign in error:', error);
}, []);
const signInState = useSignIn({
nonce: nonce || undefined,
onSuccess: onSuccessCallback,
onError: onErrorCallback,
});
const {
signIn: frontendSignIn,
signOut: frontendSignOut,
connect,
reconnect,
isSuccess,
isError,
error,
channelToken,
url,
data,
validSignature,
} = signInState;
useEffect(() => {
setMessage(data?.message || null);
setSignature(data?.signature || null);
}, [data?.message, data?.signature]);
// Connect for frontend flow when nonce is available
useEffect(() => { useEffect(() => {
if (!useBackendFlow && nonce && !channelToken) { if (!useBackendFlow && nonce && !channelToken) {
connect(); connect();
} }
}, [useBackendFlow, nonce, channelToken, connect]); }, [useBackendFlow, nonce, channelToken, connect]);
// Backend flow using QuickAuth
const handleBackendSignIn = useCallback(async () => {
if (!nonce) {
console.error('❌ No nonce available for backend sign-in');
return;
}
try {
setSignersLoading(true);
const result = await sdk.actions.signIn({ nonce });
setMessage(result.message);
setSignature(result.signature);
// Use QuickAuth to sign in
const signInResult = await quickAuthSignIn();
// Fetch user profile after sign in
if (quickAuthUser?.fid) {
const user = await fetchUserData(quickAuthUser.fid);
setBackendUserProfile({
username: user?.username || '',
pfpUrl: user?.pfp_url || '',
});
}
} catch (e) {
if (e instanceof SignInCore.RejectedByUser) {
console.log(' Sign-in rejected by user');
} else {
console.error('❌ Backend sign-in error:', e);
}
}
}, [nonce, quickAuthSignIn, quickAuthUser, fetchUserData]);
// Fetch user profile when quickAuthUser.fid changes (for backend flow)
useEffect(() => {
if (useBackendFlow && quickAuthUser?.fid) {
(async () => {
const user = await fetchUserData(quickAuthUser.fid);
setBackendUserProfile({
username: user?.username || '',
pfpUrl: user?.pfp_url || '',
});
})();
}
}, [useBackendFlow, quickAuthUser?.fid, fetchUserData]);
const handleFrontEndSignIn = useCallback(() => {
if (isError) {
reconnect();
}
setDialogStep('signin');
setShowDialog(true);
frontendSignIn();
}, [isError, reconnect, frontendSignIn]);
const handleSignOut = useCallback(async () => {
try {
setSignersLoading(true);
if (useBackendFlow) {
// Use QuickAuth sign out
await quickAuthSignOut();
} else {
// Frontend flow sign out
frontendSignOut();
removeItem(STORAGE_KEY);
setStoredAuth(null);
}
// Common cleanup for both flows
setShowDialog(false);
setDialogStep('signin');
setSignerApprovalUrl(null);
setMessage(null);
setSignature(null);
// Reset polling interval
if (pollingInterval) {
clearInterval(pollingInterval);
setPollingInterval(null);
}
// Reset signer flow flag
signerFlowStartedRef.current = false;
} catch (error) {
console.error('❌ Error during sign out:', error);
// Optionally handle error state
} finally {
setSignersLoading(false);
}
}, [useBackendFlow, frontendSignOut, pollingInterval, quickAuthSignOut]);
// Handle fetching signers after successful authentication // Handle fetching signers after successful authentication
useEffect(() => { useEffect(() => {
if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) { if (message && signature) {
signerFlowStartedRef.current = true;
const handleSignerFlow = async () => { const handleSignerFlow = async () => {
setIsSignerFlowRunning(true);
try { try {
const clientContext = context?.client as Record<string, unknown>; const clientContext = context?.client as Record<string, unknown>;
const isMobileContext = const isMobileContext =
@ -560,7 +436,6 @@ export function NeynarAuthButton() {
// First, fetch existing signers // First, fetch existing signers
const signers = await fetchAllSigners(message, signature); const signers = await fetchAllSigners(message, signature);
if (useBackendFlow && isMobileContext) setSignersLoading(true); if (useBackendFlow && isMobileContext) setSignersLoading(true);
// Check if no signers exist or if we have empty signers // Check if no signers exist or if we have empty signers
@ -581,8 +456,8 @@ export function NeynarAuthButton() {
setShowDialog(false); setShowDialog(false);
await sdk.actions.openUrl( await sdk.actions.openUrl(
signedKeyData.signer_approval_url.replace( signedKeyData.signer_approval_url.replace(
'https://client.farcaster.xyz/deeplinks/signed-key-request', 'https://client.farcaster.xyz/deeplinks/',
'https://farcaster.xyz/~/connect' 'farcaster://'
) )
); );
} else { } else {
@ -605,27 +480,116 @@ export function NeynarAuthButton() {
setSignersLoading(false); setSignersLoading(false);
setShowDialog(false); setShowDialog(false);
setSignerApprovalUrl(null); setSignerApprovalUrl(null);
} finally {
setIsSignerFlowRunning(false);
} }
}; };
handleSignerFlow(); handleSignerFlow();
} }
}, [message, signature]); // Simplified dependencies }, [
message,
signature,
fetchAllSigners,
createSigner,
generateSignedKeyRequest,
startPolling,
context,
useBackendFlow,
]);
// Backend flow using NextAuth
const handleBackendSignIn = useCallback(async () => {
if (!nonce) {
console.error('❌ No nonce available for backend sign-in');
return;
}
try {
setSignersLoading(true);
const result = await sdk.actions.signIn({ nonce });
const signInData = {
message: result.message,
signature: result.signature,
redirect: false,
nonce: nonce,
};
const nextAuthResult = await backendSignIn('neynar', signInData);
if (nextAuthResult?.ok) {
setMessage(result.message);
setSignature(result.signature);
} else {
console.error('❌ NextAuth sign-in failed:', nextAuthResult);
}
} catch (e) {
if (e instanceof SignInCore.RejectedByUser) {
console.log(' Sign-in rejected by user');
} else {
console.error('❌ Backend sign-in error:', e);
}
}
}, [nonce]);
const handleFrontEndSignIn = useCallback(() => {
if (isError) {
reconnect();
}
setDialogStep('signin');
setShowDialog(true);
frontendSignIn();
}, [isError, reconnect, frontendSignIn]);
const handleSignOut = useCallback(async () => {
try {
setSignersLoading(true);
if (useBackendFlow) {
// Only sign out from NextAuth if the current session is from Neynar provider
if (session?.provider === 'neynar') {
await backendSignOut({ redirect: false });
}
} else {
// Frontend flow sign out
frontendSignOut();
removeItem(STORAGE_KEY);
setStoredAuth(null);
}
// Common cleanup for both flows
setShowDialog(false);
setDialogStep('signin');
setSignerApprovalUrl(null);
setMessage(null);
setSignature(null);
// Reset polling interval
if (pollingInterval) {
clearInterval(pollingInterval);
setPollingInterval(null);
}
} catch (error) {
console.error('❌ Error during sign out:', error);
// Optionally handle error state
} finally {
setSignersLoading(false);
}
}, [useBackendFlow, frontendSignOut, pollingInterval, session]);
// Authentication check based on flow type
const authenticated = useBackendFlow const authenticated = useBackendFlow
? !!quickAuthUser?.fid // QuickAuth flow: check if user is authenticated via QuickAuth ? !!(
: ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && // Auth-kit flow: check auth-kit success or stored auth session?.provider === 'neynar' &&
!!(storedAuth?.signers && storedAuth.signers.length > 0); // Both flows: ensure signers exist session?.user?.fid &&
session?.signers &&
session.signers.length > 0
)
: ((isSuccess && validSignature) || storedAuth?.isAuthenticated) &&
!!(storedAuth?.signers && storedAuth.signers.length > 0);
// User data based on flow type
const userData = useBackendFlow const userData = useBackendFlow
? { ? {
fid: quickAuthUser?.fid, fid: session?.user?.fid,
username: backendUserProfile.username ?? '', username: session?.user?.username || '',
pfpUrl: backendUserProfile.pfpUrl ?? '', pfpUrl: session?.user?.pfp_url || '',
} }
: { : {
fid: storedAuth?.user?.fid, fid: storedAuth?.user?.fid,
@ -654,7 +618,7 @@ export function NeynarAuthButton() {
) : ( ) : (
<Button <Button
onClick={useBackendFlow ? handleBackendSignIn : handleFrontEndSignIn} onClick={useBackendFlow ? handleBackendSignIn : handleFrontEndSignIn}
disabled={!useBackendFlow && !url} // Disable button in browser flow if auth-kit URL is not ready disabled={!useBackendFlow && !url}
className={cn( className={cn(
'btn btn-primary flex items-center gap-3', 'btn btn-primary flex items-center gap-3',
'disabled:opacity-50 disabled:cursor-not-allowed', 'disabled:opacity-50 disabled:cursor-not-allowed',

View File

@ -7,7 +7,16 @@ import { Button } from "../Button";
import { SignIn } from "../wallet/SignIn"; import { SignIn } from "../wallet/SignIn";
import { type Haptics } from "@farcaster/miniapp-sdk"; import { type Haptics } from "@farcaster/miniapp-sdk";
import { APP_URL } from "~/lib/constants"; import { APP_URL } from "~/lib/constants";
import { NeynarAuthButton } from '../NeynarAuthButton/index';
// Optional import for NeynarAuthButton - may not exist in all templates
let NeynarAuthButton: React.ComponentType | null = null;
try {
const module = require('../NeynarAuthButton/index');
NeynarAuthButton = module.NeynarAuthButton;
} catch (error) {
// Component doesn't exist, that's okay
console.log('NeynarAuthButton not available in this template');
}
/** /**
* ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback.
@ -140,7 +149,7 @@ export function ActionsTab() {
<SignIn /> <SignIn />
{/* Neynar Authentication */} {/* Neynar Authentication */}
<NeynarAuthButton /> {NeynarAuthButton && <NeynarAuthButton />}
{/* Mini app actions */} {/* Mini app actions */}
<Button <Button