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 */}