diff --git a/.eslintrc.json b/.eslintrc.json index 3722418..24a1239 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,3 +1,32 @@ { - "extends": ["next/core-web-vitals", "next/typescript"] + "extends": ["next/core-web-vitals", "next/typescript"], + "rules": { + // Disable img warnings since you're using them intentionally in specific contexts + "@next/next/no-img-element": "off", + + // Allow @ts-ignore comments (though @ts-expect-error is preferred) + "@typescript-eslint/ban-ts-comment": "off", + + // Allow explicit any types (sometimes necessary for dynamic imports and APIs) + "@typescript-eslint/no-explicit-any": "off", + + // Allow unused variables that start with underscore + "@typescript-eslint/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + + // Make display name warnings instead of errors for dynamic components + "react/display-name": "warn", + + // Allow module assignment for dynamic imports + "@next/next/no-assign-module-variable": "warn", + + // Make exhaustive deps a warning instead of error for complex hooks + "react-hooks/exhaustive-deps": "warn" + } } diff --git a/package.json b/package.json index e7340c5..bbb40c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.7.10", + "version": "1.7.11", "type": "module", "private": false, "access": "public", @@ -35,7 +35,7 @@ "build:raw": "next build", "start": "next start", "lint": "next lint", - "deploy:vercel": "ts-node scripts/deploy.ts", + "deploy:vercel": "node --loader ts-node/esm scripts/deploy.ts", "deploy:raw": "vercel --prod", "cleanup": "node scripts/cleanup.js" }, @@ -52,4 +52,4 @@ "@types/node": "^22.13.10", "typescript": "^5.6.3" } -} +} \ No newline at end of file diff --git a/scripts/deploy.ts b/scripts/deploy.ts index a96a637..4fb429a 100755 --- a/scripts/deploy.ts +++ b/scripts/deploy.ts @@ -7,7 +7,7 @@ import inquirer from 'inquirer'; import dotenv from 'dotenv'; import crypto from 'crypto'; import { Vercel } from '@vercel/sdk'; -import { APP_NAME, APP_BUTTON_TEXT } from '../src/lib/constants'; +import { APP_NAME, APP_BUTTON_TEXT } from '../src/lib/constants.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index c42d41a..6b3ad05 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -22,9 +22,8 @@ export default async function RootLayout({ let session = null; if (shouldUseSession) { try { - // @ts-ignore - auth module may not exist in all template variants - const authModule = eval('require("~/auth")'); - session = await authModule.getSession(); + const { getSession } = await import('~/auth'); + session = await getSession(); } catch (error) { console.warn('Failed to get session:', error); } diff --git a/src/app/providers.tsx b/src/app/providers.tsx index d9c19fa..7804fcb 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -4,6 +4,7 @@ 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'), @@ -12,7 +13,94 @@ 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, @@ -26,47 +114,6 @@ export function Providers({ const solanaEndpoint = process.env.SOLANA_RPC_ENDPOINT || 'https://solana-rpc.publicnode.com'; - // Only wrap with SessionProvider if next auth is used - if (shouldUseSession) { - // Dynamic import for auth components - will work if modules exist, fallback if not - const AuthWrapper = dynamic( - () => { - return Promise.resolve().then(() => { - // Use eval to avoid build-time module resolution - try { - // @ts-ignore - These modules may not exist in all template variants - const nextAuth = eval('require("next-auth/react")'); - const authKit = eval('require("@farcaster/auth-kit")'); - - return ({ children }: { children: React.ReactNode }) => ( - - {children} - - ); - } catch (error) { - // Fallback component when auth modules aren't available - return ({ children }: { children: React.ReactNode }) => <>{children}; - } - }); - }, - { ssr: false } - ); - - return ( - - - - {children} - - - - ); - } - - // Return without SessionProvider if no session return ( - {children} + + {children} + diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index bc7880d..6b9639e 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -1,7 +1,7 @@ 'use client'; import dynamic from 'next/dynamic'; -import { useCallback, useState, type ComponentType } from 'react'; +import { useCallback, useState } from 'react'; import { useMiniApp } from '@neynar/react'; import { ShareButton } from '../Share'; import { Button } from '../Button'; @@ -9,24 +9,15 @@ import { SignIn } from '../wallet/SignIn'; import { type Haptics } from '@farcaster/miniapp-sdk'; import { APP_URL } from '~/lib/constants'; -// Optional import for NeynarAuthButton - may not exist in all templates +// Import NeynarAuthButton const NeynarAuthButton = dynamic( - () => { - return Promise.resolve().then(() => { - try { - // @ts-ignore - NeynarAuthButton may not exist in all template variants - const module = eval('require("../NeynarAuthButton/index")'); - return module.default || module.NeynarAuthButton; - } catch (error) { - // Return null component when module doesn't exist - return () => null; - } - }); - }, + () => + import('../NeynarAuthButton').then((module) => ({ + default: module.NeynarAuthButton, + })), { ssr: false } ); - /** * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. * diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 4938795..27a4524 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,4 +1,4 @@ -import { type AccountAssociation } from '@farcaster/miniapp-node'; +import { type AccountAssociation } from '@farcaster/miniapp-core/src/manifest'; /** * Application constants and configuration values. @@ -65,21 +65,22 @@ 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 --- /** * Text displayed on the main action button. * Used for the primary call-to-action in the mini app. */ -export const APP_BUTTON_TEXT: string = 'Launch NSK'; +export const APP_BUTTON_TEXT = 'Launch Mini App'; // --- Integration Configuration --- /** @@ -89,7 +90,8 @@ 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`; @@ -100,7 +102,7 @@ export const APP_WEBHOOK_URL: string = process.env.NEYNAR_API_KEY && process.env * When false, wallet functionality is completely hidden from the UI. * Useful for mini apps that don't require wallet integration. */ -export const USE_WALLET: boolean = true; +export const USE_WALLET = false; /** * Flag to enable/disable analytics tracking. @@ -109,7 +111,7 @@ export const USE_WALLET: boolean = true; * When false, analytics collection is disabled. * Useful for privacy-conscious users or development environments. */ -export const ANALYTICS_ENABLED: boolean = true; +export const ANALYTICS_ENABLED = true; /** * Required chains for the mini app. @@ -117,7 +119,7 @@ export const ANALYTICS_ENABLED: boolean = true; * Contains an array of CAIP-2 identifiers for blockchains that the mini app requires. * If the host does not support all chains listed here, it will not render the mini app. * If empty or undefined, the mini app will be rendered regardless of chain support. - * + * * Supported chains: eip155:1, eip155:137, eip155:42161, eip155:10, eip155:8453, * solana:mainnet, solana:devnet */ diff --git a/src/lib/kv.ts b/src/lib/kv.ts index eefc680..28f0982 100644 --- a/src/lib/kv.ts +++ b/src/lib/kv.ts @@ -1,16 +1,18 @@ -import { FrameNotificationDetails } from "@farcaster/miniapp-sdk"; -import { Redis } from "@upstash/redis"; -import { APP_NAME } from "./constants"; +import { MiniAppNotificationDetails } from '@farcaster/miniapp-sdk'; +import { Redis } from '@upstash/redis'; +import { APP_NAME } from './constants'; // In-memory fallback storage -const localStore = new Map(); +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}`; @@ -18,17 +20,17 @@ function getUserNotificationDetailsKey(fid: number): string { export async function getUserNotificationDetails( fid: number -): Promise { +): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { - return await redis.get(key); + return await redis.get(key); } return localStore.get(key) || null; } export async function setUserNotificationDetails( fid: number, - notificationDetails: FrameNotificationDetails + notificationDetails: MiniAppNotificationDetails ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 89e8b8c..8fb8a40 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; -import { type Manifest } from '@farcaster/miniapp-node'; +import { Manifest } from '@farcaster/miniapp-core/src/manifest'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, @@ -10,10 +10,10 @@ 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, - APP_REQUIRED_CHAINS, } from './constants'; export function cn(...inputs: ClassValue[]) { @@ -22,7 +22,7 @@ export function cn(...inputs: ClassValue[]) { export function getMiniAppEmbedMetadata(ogImageUrl?: string) { return { - version: "next", + version: 'next', imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL, ogTitle: APP_NAME, ogDescription: APP_DESCRIPTION, @@ -30,7 +30,7 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) { button: { title: APP_BUTTON_TEXT, action: { - type: "launch_frame", + type: 'launch_frame', name: APP_NAME, url: APP_URL, splashImageUrl: APP_SPLASH_URL, @@ -46,24 +46,17 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) { export async function getFarcasterDomainManifest(): Promise { return { - accountAssociation: APP_ACCOUNT_ASSOCIATION, + accountAssociation: APP_ACCOUNT_ASSOCIATION!, miniapp: { - version: "1", - name: APP_NAME ?? "Neynar Starter Kit", - iconUrl: APP_ICON_URL, + version: '1', + name: APP_NAME ?? 'Neynar Starter Kit', homeUrl: APP_URL, + iconUrl: APP_ICON_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, - description: APP_DESCRIPTION, - primaryCategory: APP_PRIMARY_CATEGORY, - tags: APP_TAGS, - requiredChains: APP_REQUIRED_CHAINS.length > 0 ? APP_REQUIRED_CHAINS : undefined, - ogTitle: APP_NAME, - ogDescription: APP_DESCRIPTION, - ogImageUrl: APP_OG_IMAGE_URL, }, }; } diff --git a/tsconfig.json b/tsconfig.json index df87999..0060acb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,13 @@ "~/*": ["./src/*"] } }, + "ts-node": { + "esm": true, + "compilerOptions": { + "module": "ES2020", + "moduleResolution": "node" + } + }, "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], "exclude": ["node_modules"] }