mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
Merge pull request #18 from neynarxyz/veganbeef/fix-siwn
fix: add back auth-kit
This commit is contained in:
commit
78626c2dc7
16
bin/init.js
16
bin/init.js
@ -460,7 +460,6 @@ export async function init(
|
|||||||
// 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',
|
||||||
@ -487,6 +486,12 @@ export async function init(
|
|||||||
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',
|
||||||
@ -699,6 +704,15 @@ export async function init(
|
|||||||
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 });
|
||||||
|
|||||||
@ -1,13 +1,21 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useCallback, useEffect, useState, useRef } from 'react';
|
import '@farcaster/auth-kit/styles.css';
|
||||||
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk';
|
import { useSignIn } from '@farcaster/auth-kit';
|
||||||
import { useMiniApp } from '@neynar/react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { cn } from '~/lib/utils';
|
||||||
import { Button } from '~/components/ui/Button';
|
import { Button } from '~/components/ui/Button';
|
||||||
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
|
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
|
||||||
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
|
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
|
||||||
import { useQuickAuth } from '~/hooks/useQuickAuth';
|
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
|
||||||
import { cn } from '~/lib/utils';
|
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';
|
||||||
|
|
||||||
type User = {
|
type User = {
|
||||||
fid: number;
|
fid: number;
|
||||||
@ -17,6 +25,7 @@ type User = {
|
|||||||
// Add other user properties as needed
|
// Add other user properties as needed
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'neynar_authenticated_user';
|
||||||
const FARCASTER_FID = 9152;
|
const FARCASTER_FID = 9152;
|
||||||
|
|
||||||
interface StoredAuthState {
|
interface StoredAuthState {
|
||||||
@ -90,12 +99,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 {
|
const { data: session } = useSession();
|
||||||
authenticatedUser: quickAuthUser,
|
|
||||||
signIn: quickAuthSignIn,
|
|
||||||
signOut: quickAuthSignOut,
|
|
||||||
} = useQuickAuth();
|
|
||||||
|
|
||||||
// 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,12 +113,6 @@ 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
|
||||||
const useBackendFlow = context !== undefined;
|
const useBackendFlow = context !== undefined;
|
||||||
@ -147,15 +145,25 @@ 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
|
// Helper function to fetch user data from Neynar API
|
||||||
@ -237,18 +245,14 @@ 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;
|
||||||
} else {
|
} else {
|
||||||
// For frontend flow, store in memory only
|
// For frontend flow, store in localStorage
|
||||||
let user: StoredAuthState['user'] | null = null;
|
let user: StoredAuthState['user'] | null = null;
|
||||||
|
|
||||||
if (signerData.signers && signerData.signers.length > 0) {
|
if (signerData.signers && signerData.signers.length > 0) {
|
||||||
@ -258,12 +262,13 @@ export function NeynarAuthButton() {
|
|||||||
user = fetchedUser;
|
user = fetchedUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store signers in memory only
|
// Store signers in localStorage, preserving existing auth data
|
||||||
const updatedState: StoredAuthState = {
|
const updatedState: StoredAuthState = {
|
||||||
isAuthenticated: !!user,
|
isAuthenticated: !!user,
|
||||||
signers: signerData.signers || [],
|
signers: signerData.signers || [],
|
||||||
user,
|
user,
|
||||||
};
|
};
|
||||||
|
setItem<StoredAuthState>(STORAGE_KEY, updatedState);
|
||||||
setStoredAuth(updatedState);
|
setStoredAuth(updatedState);
|
||||||
|
|
||||||
return signerData.signers;
|
return signerData.signers;
|
||||||
@ -285,46 +290,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();
|
||||||
@ -346,7 +319,7 @@ export function NeynarAuthButton() {
|
|||||||
|
|
||||||
setPollingInterval(interval);
|
setPollingInterval(interval);
|
||||||
},
|
},
|
||||||
[fetchAllSigners, pollingInterval],
|
[fetchAllSigners]
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cleanup polling on unmount
|
// Cleanup polling on unmount
|
||||||
@ -355,7 +328,6 @@ export function NeynarAuthButton() {
|
|||||||
if (pollingInterval) {
|
if (pollingInterval) {
|
||||||
clearInterval(pollingInterval);
|
clearInterval(pollingInterval);
|
||||||
}
|
}
|
||||||
signerFlowStartedRef.current = false;
|
|
||||||
};
|
};
|
||||||
}, [pollingInterval]);
|
}, [pollingInterval]);
|
||||||
|
|
||||||
@ -378,118 +350,78 @@ export function NeynarAuthButton() {
|
|||||||
generateNonce();
|
generateNonce();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Backend flow using QuickAuth
|
// Load stored auth state on mount (only for frontend flow)
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [nonce, quickAuthSignIn, quickAuthUser, fetchUserData]);
|
|
||||||
|
|
||||||
// Fetch user profile when quickAuthUser.fid changes (for backend flow)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (useBackendFlow && quickAuthUser?.fid) {
|
if (!useBackendFlow) {
|
||||||
(async () => {
|
const stored = getItem<StoredAuthState>(STORAGE_KEY);
|
||||||
const user = await fetchUserData(quickAuthUser.fid);
|
if (stored && stored.isAuthenticated) {
|
||||||
setBackendUserProfile({
|
setStoredAuth(stored);
|
||||||
username: user?.username || '',
|
|
||||||
pfpUrl: user?.pfp_url || '',
|
|
||||||
});
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
}, [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]);
|
}, [useBackendFlow]);
|
||||||
|
|
||||||
const handleSignOut = useCallback(async () => {
|
// Success callback - this is critical!
|
||||||
try {
|
const onSuccessCallback = useCallback(
|
||||||
setSignersLoading(true);
|
async (res: unknown) => {
|
||||||
|
if (!useBackendFlow) {
|
||||||
if (useBackendFlow) {
|
// Only handle localStorage for frontend flow
|
||||||
// Use QuickAuth sign out
|
const existingAuth = getItem<StoredAuthState>(STORAGE_KEY);
|
||||||
await quickAuthSignOut();
|
const user = await fetchUserData(res.fid);
|
||||||
} else {
|
const authState: StoredAuthState = {
|
||||||
// Frontend flow sign out
|
...existingAuth,
|
||||||
setStoredAuth(null);
|
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, fetchUserData]
|
||||||
|
);
|
||||||
|
|
||||||
// Common cleanup for both flows
|
// Error callback
|
||||||
setShowDialog(false);
|
const onErrorCallback = useCallback((error?: Error | null) => {
|
||||||
setDialogStep('signin');
|
console.error('❌ Sign in error:', error);
|
||||||
setSignerApprovalUrl(null);
|
}, []);
|
||||||
setMessage(null);
|
|
||||||
setSignature(null);
|
|
||||||
|
|
||||||
// Reset polling interval
|
const signInState = useSignIn({
|
||||||
if (pollingInterval) {
|
nonce: nonce || undefined,
|
||||||
clearInterval(pollingInterval);
|
onSuccess: onSuccessCallback,
|
||||||
setPollingInterval(null);
|
onError: onErrorCallback,
|
||||||
}
|
});
|
||||||
|
|
||||||
// Reset signer flow flag
|
const {
|
||||||
signerFlowStartedRef.current = false;
|
signIn: frontendSignIn,
|
||||||
} catch (error) {
|
signOut: frontendSignOut,
|
||||||
console.error('❌ Error during sign out:', error);
|
connect,
|
||||||
// Optionally handle error state
|
reconnect,
|
||||||
} finally {
|
isSuccess,
|
||||||
setSignersLoading(false);
|
isError,
|
||||||
|
error,
|
||||||
|
channelToken,
|
||||||
|
url,
|
||||||
|
data,
|
||||||
|
validSignature,
|
||||||
|
} = signInState;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setMessage(data?.message || null);
|
||||||
|
setSignature(data?.signature || null);
|
||||||
|
}, [data?.message, data?.signature]);
|
||||||
|
|
||||||
|
// Connect for frontend flow when nonce is available
|
||||||
|
useEffect(() => {
|
||||||
|
if (!useBackendFlow && nonce && !channelToken) {
|
||||||
|
connect();
|
||||||
}
|
}
|
||||||
}, [useBackendFlow, pollingInterval, quickAuthSignOut]);
|
}, [useBackendFlow, nonce, channelToken, connect]);
|
||||||
|
|
||||||
// Handle fetching signers after successful authentication
|
// Handle fetching signers after successful authentication
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (message && signature) {
|
||||||
message &&
|
|
||||||
signature &&
|
|
||||||
!isSignerFlowRunning &&
|
|
||||||
!signerFlowStartedRef.current
|
|
||||||
) {
|
|
||||||
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 =
|
||||||
@ -505,7 +437,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
|
||||||
@ -526,9 +457,9 @@ 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 {
|
||||||
setShowDialog(true); // Ensure dialog is shown during loading
|
setShowDialog(true); // Ensure dialog is shown during loading
|
||||||
@ -550,25 +481,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]);
|
||||||
|
|
||||||
const authenticated = useBackendFlow
|
const authenticated = useBackendFlow
|
||||||
? !!quickAuthUser?.fid
|
? !!(
|
||||||
: storedAuth?.isAuthenticated &&
|
session?.provider === 'neynar' &&
|
||||||
|
session?.user?.fid &&
|
||||||
|
session?.signers &&
|
||||||
|
session.signers.length > 0
|
||||||
|
)
|
||||||
|
: ((isSuccess && validSignature) || storedAuth?.isAuthenticated) &&
|
||||||
!!(storedAuth?.signers && storedAuth.signers.length > 0);
|
!!(storedAuth?.signers && storedAuth.signers.length > 0);
|
||||||
|
|
||||||
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,
|
||||||
@ -597,17 +619,18 @@ export function NeynarAuthButton() {
|
|||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={useBackendFlow ? handleBackendSignIn : handleFrontEndSignIn}
|
onClick={useBackendFlow ? handleBackendSignIn : handleFrontEndSignIn}
|
||||||
disabled={signersLoading}
|
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',
|
||||||
'transform transition-all duration-200 active:scale-[0.98]',
|
'transform transition-all duration-200 active:scale-[0.98]',
|
||||||
|
!url && !useBackendFlow && 'cursor-not-allowed'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{signersLoading ? (
|
{!useBackendFlow && !url ? (
|
||||||
<>
|
<>
|
||||||
<div className="spinner-primary w-5 h-5" />
|
<div className="spinner-primary w-5 h-5" />
|
||||||
<span>Loading...</span>
|
<span>Initializing...</span>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@ -630,9 +653,9 @@ export function NeynarAuthButton() {
|
|||||||
setPollingInterval(null);
|
setPollingInterval(null);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
url={undefined}
|
url={url}
|
||||||
isError={false}
|
isError={isError}
|
||||||
error={null}
|
error={error}
|
||||||
step={dialogStep}
|
step={dialogStep}
|
||||||
isLoading={signersLoading}
|
isLoading={signersLoading}
|
||||||
signerApprovalUrl={signerApprovalUrl}
|
signerApprovalUrl={signerApprovalUrl}
|
||||||
@ -640,4 +663,4 @@ export function NeynarAuthButton() {
|
|||||||
}
|
}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -5,10 +5,19 @@ import { type Haptics } from '@farcaster/miniapp-sdk';
|
|||||||
import { useMiniApp } from '@neynar/react';
|
import { useMiniApp } from '@neynar/react';
|
||||||
import { APP_URL } from '~/lib/constants';
|
import { APP_URL } from '~/lib/constants';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { NeynarAuthButton } from '../NeynarAuthButton/index';
|
|
||||||
import { ShareButton } from '../Share';
|
import { ShareButton } from '../Share';
|
||||||
import { SignIn } from '../wallet/SignIn';
|
import { SignIn } from '../wallet/SignIn';
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user