diff --git a/bin/init.js b/bin/init.js index c0ffd39..fb6e718 100644 --- a/bin/init.js +++ b/bin/init.js @@ -747,9 +747,12 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.rmSync(binPath, { recursive: true, force: true }); } - // Remove NeynarAuthButton directory, NextAuth API routes, and auth directory if SIWN is not enabled (no seed phrase) + // Handle SIWN-related files based on whether seed phrase is provided if (!answers.seedPhrase) { - console.log('\nRemoving NeynarAuthButton directory, NextAuth API routes, and auth directory (SIWN not enabled)...'); + // Remove SIWN-related files when SIWN is not enabled (no seed phrase) + console.log('\nRemoving SIWN-related files (SIWN not enabled)...'); + + // Remove NeynarAuthButton directory const neynarAuthButtonPath = path.join(projectPath, 'src', 'components', 'ui', 'NeynarAuthButton'); if (fs.existsSync(neynarAuthButtonPath)) { fs.rmSync(neynarAuthButtonPath, { recursive: true, force: true }); @@ -772,19 +775,56 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.rmSync(authFilePath, { force: true }); } - // Replace NeynarAuthButton import in ActionsTab.tsx with null component + // Remove SIWN-specific files + const actionsTabNeynarAuthPath = path.join(projectPath, 'src', 'components', 'ui', 'tabs', 'ActionsTab.NeynarAuth.tsx'); + if (fs.existsSync(actionsTabNeynarAuthPath)) { + fs.rmSync(actionsTabNeynarAuthPath, { force: true }); + } + + const layoutNeynarAuthPath = path.join(projectPath, 'src', 'app', 'layout.NeynarAuth.tsx'); + if (fs.existsSync(layoutNeynarAuthPath)) { + fs.rmSync(layoutNeynarAuthPath, { force: true }); + } + + const providersNeynarAuthPath = path.join(projectPath, 'src', 'app', 'providers.NeynarAuth.tsx'); + if (fs.existsSync(providersNeynarAuthPath)) { + fs.rmSync(providersNeynarAuthPath, { force: true }); + } + } else { + // Move SIWN-specific files to replace the regular versions when SIWN is enabled + console.log('\nMoving SIWN-specific files to replace regular versions (SIWN enabled)...'); + + // Move ActionsTab.NeynarAuth.tsx to ActionsTab.tsx + const actionsTabNeynarAuthPath = path.join(projectPath, 'src', 'components', 'ui', 'tabs', 'ActionsTab.NeynarAuth.tsx'); const actionsTabPath = path.join(projectPath, 'src', 'components', 'ui', 'tabs', 'ActionsTab.tsx'); - if (fs.existsSync(actionsTabPath)) { - let actionsTabContent = fs.readFileSync(actionsTabPath, 'utf8'); - - // Replace the dynamic import of NeynarAuthButton with a null component - actionsTabContent = actionsTabContent.replace( - /const NeynarAuthButton = dynamic\([\s\S]*?\);/, - '// NeynarAuthButton disabled - SIWN not enabled\nconst NeynarAuthButton = () => {\n return null;\n};' - ); - - fs.writeFileSync(actionsTabPath, actionsTabContent); - console.log('✅ Replaced NeynarAuthButton import in ActionsTab.tsx with null component'); + if (fs.existsSync(actionsTabNeynarAuthPath)) { + if (fs.existsSync(actionsTabPath)) { + fs.rmSync(actionsTabPath, { force: true }); // Delete original + } + fs.renameSync(actionsTabNeynarAuthPath, actionsTabPath); + console.log('✅ Moved ActionsTab.NeynarAuth.tsx to ActionsTab.tsx'); + } + + // Move layout.NeynarAuth.tsx to layout.tsx + const layoutNeynarAuthPath = path.join(projectPath, 'src', 'app', 'layout.NeynarAuth.tsx'); + const layoutPath = path.join(projectPath, 'src', 'app', 'layout.tsx'); + if (fs.existsSync(layoutNeynarAuthPath)) { + if (fs.existsSync(layoutPath)) { + fs.rmSync(layoutPath, { force: true }); // Delete original + } + fs.renameSync(layoutNeynarAuthPath, layoutPath); + console.log('✅ Moved layout.NeynarAuth.tsx to layout.tsx'); + } + + // Move providers.NeynarAuth.tsx to providers.tsx + const providersNeynarAuthPath = path.join(projectPath, 'src', 'app', 'providers.NeynarAuth.tsx'); + const providersPath = path.join(projectPath, 'src', 'app', 'providers.tsx'); + if (fs.existsSync(providersNeynarAuthPath)) { + if (fs.existsSync(providersPath)) { + fs.rmSync(providersPath, { force: true }); // Delete original + } + fs.renameSync(providersNeynarAuthPath, providersPath); + console.log('✅ Moved providers.NeynarAuth.tsx to providers.tsx'); } } diff --git a/package.json b/package.json index 374d313..bb5b356 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.7.14", + "version": "1.8.0", "type": "module", "private": false, "access": "public", diff --git a/src/app/layout.NeynarAuth.tsx b/src/app/layout.NeynarAuth.tsx new file mode 100644 index 0000000..bb3d5ba --- /dev/null +++ b/src/app/layout.NeynarAuth.tsx @@ -0,0 +1,29 @@ +import type { Metadata } from 'next'; + +import { getSession } from '~/auth'; +import '~/app/globals.css'; +import { Providers } from '~/app/providers'; +import { APP_NAME, APP_DESCRIPTION } from '~/lib/constants'; + +export const metadata: Metadata = { + title: APP_NAME, + description: APP_DESCRIPTION, +}; + +export default async function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + const session = await getSession(); + + return ( + + + + {children} + + + + ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 6b3ad05..c3714f6 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,25 +14,10 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { - // Only get session if sponsored signer is enabled or seed phrase is provided - const sponsorSigner = process.env.SPONSOR_SIGNER === 'true'; - const hasSeedPhrase = !!process.env.SEED_PHRASE; - const shouldUseSession = sponsorSigner || hasSeedPhrase; - - let session = null; - if (shouldUseSession) { - try { - const { getSession } = await import('~/auth'); - session = await getSession(); - } catch (error) { - console.warn('Failed to get session:', error); - } - } - return ( - + {children} diff --git a/src/app/providers.NeynarAuth.tsx b/src/app/providers.NeynarAuth.tsx new file mode 100644 index 0000000..e8bd047 --- /dev/null +++ b/src/app/providers.NeynarAuth.tsx @@ -0,0 +1,43 @@ +'use client'; + +import dynamic from 'next/dynamic'; +import type { Session } from 'next-auth'; +import { SessionProvider } from 'next-auth/react'; +import { AuthKitProvider } from '@farcaster/auth-kit'; +import { MiniAppProvider } from '@neynar/react'; +import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider'; +import { ANALYTICS_ENABLED } from '~/lib/constants'; + +const WagmiProvider = dynamic( + () => import('~/components/providers/WagmiProvider'), + { + ssr: false, + } +); + +export function Providers({ + session, + children, +}: { + session: Session | null; + children: React.ReactNode; +}) { + const solanaEndpoint = + process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com'; + return ( + + + + + + {children} + + + + + + ); +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 7804fcb..d894dd8 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -4,7 +4,6 @@ import dynamic from 'next/dynamic'; import { MiniAppProvider } from '@neynar/react'; import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider'; import { ANALYTICS_ENABLED } from '~/lib/constants'; -import React, { useState, useEffect } from 'react'; const WagmiProvider = dynamic( () => import('~/components/providers/WagmiProvider'), @@ -13,107 +12,13 @@ const WagmiProvider = dynamic( } ); -// Helper component to conditionally render auth providers -function AuthProviders({ - children, - session, - shouldUseSession, -}: { - children: React.ReactNode; - session: any; - shouldUseSession: boolean; -}) { - const [authComponents, setAuthComponents] = useState<{ - SessionProvider: React.ComponentType | null; - AuthKitProvider: React.ComponentType | null; - loaded: boolean; - }>({ - SessionProvider: null, - AuthKitProvider: null, - loaded: false, - }); - - useEffect(() => { - if (!shouldUseSession) { - setAuthComponents({ - SessionProvider: null, - AuthKitProvider: null, - loaded: true, - }); - return; - } - - const loadAuthComponents = async () => { - try { - // Dynamic imports for auth modules - let SessionProvider = null; - let AuthKitProvider = null; - - try { - const nextAuth = await import('next-auth/react'); - SessionProvider = nextAuth.SessionProvider; - } catch (error) { - console.warn('NextAuth not available:', error); - } - - try { - const authKit = await import('@farcaster/auth-kit'); - AuthKitProvider = authKit.AuthKitProvider; - } catch (error) { - console.warn('Farcaster AuthKit not available:', error); - } - - setAuthComponents({ - SessionProvider, - AuthKitProvider, - loaded: true, - }); - } catch (error) { - console.error('Error loading auth components:', error); - setAuthComponents({ - SessionProvider: null, - AuthKitProvider: null, - loaded: true, - }); - } - }; - - loadAuthComponents(); - }, [shouldUseSession]); - - if (!authComponents.loaded) { - return <>; - } - - if (!shouldUseSession || !authComponents.SessionProvider) { - return <>{children}; - } - - const { SessionProvider, AuthKitProvider } = authComponents; - - if (AuthKitProvider) { - return ( - - {children} - - ); - } - - return {children}; -} - export function Providers({ - session, children, - shouldUseSession = false, }: { - session: any | null; children: React.ReactNode; - shouldUseSession?: boolean; }) { const solanaEndpoint = process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com'; - return ( - + {children} - + diff --git a/src/components/ui/tabs/ActionsTab.NeynarAuth.tsx b/src/components/ui/tabs/ActionsTab.NeynarAuth.tsx new file mode 100644 index 0000000..fc34642 --- /dev/null +++ b/src/components/ui/tabs/ActionsTab.NeynarAuth.tsx @@ -0,0 +1,208 @@ +'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 { NeynarAuthButton } from '../NeynarAuthButton'; + +/** + * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. + * + * This component provides the main interaction interface for users to: + * - Share the mini app with others + * - Sign in with Farcaster + * - Send notifications to their account + * - Trigger haptic feedback + * - Add the mini app to their client + * - Copy share URLs + * + * The component uses the useMiniApp hook to access Farcaster context and actions. + * All state is managed locally within this component. + * + * @example + * ```tsx + * + * ``` + */ +export function ActionsTab() { + // --- Hooks --- + const { actions, added, notificationDetails, haptics, context } = + useMiniApp(); + + // --- State --- + const [notificationState, setNotificationState] = useState({ + sendStatus: '', + shareUrlCopied: false, + }); + const [selectedHapticIntensity, setSelectedHapticIntensity] = + useState('medium'); + + // --- Handlers --- + /** + * Sends a notification to the current user's Farcaster account. + * + * This function makes a POST request to the /api/send-notification endpoint + * with the user's FID and notification details. It handles different response + * statuses including success (200), rate limiting (429), and errors. + * + * @returns Promise that resolves when the notification is sent or fails + */ + const sendFarcasterNotification = useCallback(async () => { + setNotificationState((prev) => ({ ...prev, sendStatus: '' })); + if (!notificationDetails || !context) { + return; + } + try { + const response = await fetch('/api/send-notification', { + method: 'POST', + mode: 'same-origin', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + fid: context.user.fid, + notificationDetails, + }), + }); + if (response.status === 200) { + setNotificationState((prev) => ({ ...prev, sendStatus: 'Success' })); + return; + } else if (response.status === 429) { + setNotificationState((prev) => ({ + ...prev, + sendStatus: 'Rate limited', + })); + return; + } + const responseText = await response.text(); + setNotificationState((prev) => ({ + ...prev, + sendStatus: `Error: ${responseText}`, + })); + } catch (error) { + setNotificationState((prev) => ({ + ...prev, + sendStatus: `Error: ${error}`, + })); + } + }, [context, notificationDetails]); + + /** + * Copies the share URL for the current user to the clipboard. + * + * This function generates a share URL using the user's FID and copies it + * to the clipboard. It shows a temporary "Copied!" message for 2 seconds. + */ + const copyUserShareUrl = useCallback(async () => { + if (context?.user?.fid) { + const userShareUrl = `${APP_URL}/share/${context.user.fid}`; + await navigator.clipboard.writeText(userShareUrl); + setNotificationState((prev) => ({ ...prev, shareUrlCopied: true })); + setTimeout( + () => + setNotificationState((prev) => ({ ...prev, shareUrlCopied: false })), + 2000 + ); + } + }, [context?.user?.fid]); + + /** + * Triggers haptic feedback with the selected intensity. + * + * This function calls the haptics.impactOccurred method with the current + * selectedHapticIntensity setting. It handles errors gracefully by logging them. + */ + const triggerHapticFeedback = useCallback(async () => { + try { + await haptics.impactOccurred(selectedHapticIntensity); + } catch (error) { + console.error('Haptic feedback failed:', error); + } + }, [haptics, selectedHapticIntensity]); + + // --- Render --- + return ( +
+ {/* Share functionality */} + + + {/* Authentication */} + + + {/* Neynar Authentication */} + + + {/* Mini app actions */} + + + + + {/* Notification functionality */} + {notificationState.sendStatus && ( +
+ Send notification result: {notificationState.sendStatus} +
+ )} + + + {/* Share URL copying */} + + + {/* Haptic feedback controls */} +
+ + + +
+
+ ); +} diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 6b9639e..dfb0fa4 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -1,6 +1,5 @@ 'use client'; -import dynamic from 'next/dynamic'; import { useCallback, useState } from 'react'; import { useMiniApp } from '@neynar/react'; import { ShareButton } from '../Share'; @@ -9,15 +8,6 @@ import { SignIn } from '../wallet/SignIn'; import { type Haptics } from '@farcaster/miniapp-sdk'; import { APP_URL } from '~/lib/constants'; -// Import NeynarAuthButton -const NeynarAuthButton = dynamic( - () => - import('../NeynarAuthButton').then((module) => ({ - default: module.NeynarAuthButton, - })), - { ssr: false } -); - /** * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. * @@ -148,9 +138,6 @@ export function ActionsTab() { {/* Authentication */} - {/* Neynar Authentication */} - {NeynarAuthButton && } - {/* Mini app actions */}