From e6654a5e5b0e034af1caa665ddc879916b5b8151 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 2 Jul 2025 14:10:30 +0530 Subject: [PATCH 01/32] fix: session and siwf result ui --- package-lock.json | 4 +-- src/components/ui/wallet/SignIn.tsx | 38 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2acd582..e6d8af4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.2.5", + "version": "1.5.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neynar/create-farcaster-mini-app", - "version": "1.2.5", + "version": "1.5.2", "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index 2d049b0..d28c66a 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -8,17 +8,17 @@ import { Button } from "../Button"; /** * SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF). - * + * * This component provides a complete authentication flow for Farcaster users: * - Generates nonces for secure authentication * - Handles the SIWF flow using the Farcaster SDK * - Manages NextAuth session state * - Provides sign-out functionality * - Displays authentication status and results - * + * * The component integrates with both the Farcaster Frame SDK and NextAuth * to provide seamless authentication within mini apps. - * + * * @example * ```tsx * @@ -45,11 +45,11 @@ export function SignIn() { // --- Handlers --- /** * Generates a nonce for the sign-in process. - * + * * This function retrieves a CSRF token from NextAuth to use as a nonce * for the SIWF authentication flow. The nonce ensures the authentication * request is fresh and prevents replay attacks. - * + * * @returns Promise - The generated nonce token * @throws Error if unable to generate nonce */ @@ -61,13 +61,13 @@ export function SignIn() { /** * Handles the sign-in process using Farcaster SDK. - * + * * This function orchestrates the complete SIWF flow: * 1. Generates a nonce for security * 2. Calls the Farcaster SDK to initiate sign-in * 3. Submits the result to NextAuth for session management * 4. Handles various error conditions including user rejection - * + * * @returns Promise */ const handleSignIn = useCallback(async () => { @@ -95,10 +95,10 @@ export function SignIn() { /** * Handles the sign-out process. - * + * * This function clears the NextAuth session and resets the local * sign-in result state to complete the sign-out flow. - * + * * @returns Promise */ const handleSignOut = useCallback(async () => { @@ -128,9 +128,9 @@ export function SignIn() { {/* Session Information */} {session && ( -
-
Session
-
+
+
Session
+
{JSON.stringify(session, null, 2)}
@@ -138,21 +138,21 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && ( -
-
SIWF Result
-
{signInFailure}
+
+
SIWF Result
+
{signInFailure}
)} {/* Success Result Display */} {signInResult && !authState.signingIn && ( -
-
SIWF Result
-
+
+
SIWF Result
+
{JSON.stringify(signInResult, null, 2)}
)} ); -} \ No newline at end of file +} From ab42334f2c4caef4eb2ac3cff238f57c727cabbd Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 2 Jul 2025 14:21:14 +0530 Subject: [PATCH 02/32] singlequotes --- src/components/ui/wallet/SignIn.tsx | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index d28c66a..32576fd 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -1,10 +1,10 @@ -"use client"; +'use client'; -import { useCallback, useState } from "react"; -import { signIn, signOut, getCsrfToken } from "next-auth/react"; -import sdk, { SignIn as SignInCore } from "@farcaster/frame-sdk"; -import { useSession } from "next-auth/react"; -import { Button } from "../Button"; +import { useCallback, useState } from 'react'; +import { signIn, signOut, getCsrfToken } from 'next-auth/react'; +import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; +import { useSession } from 'next-auth/react'; +import { Button } from '../Button'; /** * SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF). @@ -55,7 +55,7 @@ export function SignIn() { */ const getNonce = useCallback(async () => { const nonce = await getCsrfToken(); - if (!nonce) throw new Error("Unable to generate nonce"); + if (!nonce) throw new Error('Unable to generate nonce'); return nonce; }, []); @@ -77,17 +77,17 @@ export function SignIn() { const nonce = await getNonce(); const result = await sdk.actions.signIn({ nonce }); setSignInResult(result); - await signIn("credentials", { + await signIn('credentials', { message: result.message, signature: result.signature, redirect: false, }); } catch (e) { if (e instanceof SignInCore.RejectedByUser) { - setSignInFailure("Rejected by user"); + setSignInFailure('Rejected by user'); return; } - setSignInFailure("Unknown error"); + setSignInFailure('Unknown error'); } finally { setAuthState((prev) => ({ ...prev, signingIn: false })); } @@ -115,12 +115,12 @@ export function SignIn() { return ( <> {/* Authentication Buttons */} - {status !== "authenticated" && ( + {status !== 'authenticated' && ( )} - {status === "authenticated" && ( + {status === 'authenticated' && ( From 857125035f444a21e006bf7cad1bcf041cfdfc72 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Wed, 2 Jul 2025 14:43:30 +0530 Subject: [PATCH 03/32] fix: session and siwf result ui --- src/components/ui/wallet/SignIn.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index 32576fd..890ab9f 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -128,7 +128,7 @@ export function SignIn() { {/* Session Information */} {session && ( -
+
Session
{JSON.stringify(session, null, 2)} @@ -138,7 +138,7 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && ( -
+
SIWF Result
{signInFailure}
@@ -146,7 +146,7 @@ export function SignIn() { {/* Success Result Display */} {signInResult && !authState.signingIn && ( -
+
SIWF Result
{JSON.stringify(signInResult, null, 2)} From 1fe4d3013448940a43037543e1a80e401f2f4fd1 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 4 Jul 2025 03:17:49 +0530 Subject: [PATCH 04/32] NeynarAuthNutton --- package-lock.json | 180 ++++++++- package.json | 1 + src/app/api/auth/nonce/route.ts | 10 + src/app/api/auth/signer/route.ts | 39 ++ src/app/providers.tsx | 35 +- src/components/ui/NeynarAuthButton.tsx | 526 +++++++++++++++++++++++++ src/components/ui/tabs/ActionsTab.tsx | 141 ++++--- 7 files changed, 861 insertions(+), 71 deletions(-) create mode 100644 src/app/api/auth/nonce/route.ts create mode 100644 src/app/api/auth/signer/route.ts create mode 100644 src/components/ui/NeynarAuthButton.tsx diff --git a/package-lock.json b/package-lock.json index e6d8af4..21e3f9b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,16 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.2", + "version": "1.5.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.2", + "version": "1.5.3", "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", + "siwe": "^3.0.0", "viem": "^2.23.6" }, "bin": { @@ -624,6 +625,47 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@spruceid/siwe-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-3.0.0.tgz", + "integrity": "sha512-Y92k63ilw/8jH9Ry4G2e7lQd0jZAvb0d/Q7ssSD0D9mp/Zt2aCXIc3g0ny9yhplpAx1QXHsMz/JJptHK/zDGdw==", + "license": "Apache-2.0", + "dependencies": { + "@noble/hashes": "^1.1.2", + "apg-js": "^4.4.0" + } + }, + "node_modules/@stablelib/binary": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", + "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", + "license": "MIT", + "dependencies": { + "@stablelib/int": "^1.0.1" + } + }, + "node_modules/@stablelib/int": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", + "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==", + "license": "MIT" + }, + "node_modules/@stablelib/random": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", + "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", + "license": "MIT", + "dependencies": { + "@stablelib/binary": "^1.0.1", + "@stablelib/wipe": "^1.0.1" + } + }, + "node_modules/@stablelib/wipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", + "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", + "license": "MIT" + }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -662,6 +704,13 @@ } } }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", + "license": "MIT", + "peer": true + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -711,6 +760,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/apg-js": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.4.0.tgz", + "integrity": "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==", + "license": "BSD-2-Clause" + }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -1318,6 +1373,114 @@ "node": ">=0.10.0" } }, + "node_modules/ethers": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", + "integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "22.7.5", + "aes-js": "4.0.0-beta.5", + "tslib": "2.7.0", + "ws": "8.17.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "license": "MIT", + "peer": true + }, + "node_modules/ethers/node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "license": "MIT", + "peer": true, + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "22.7.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", + "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~6.19.2" + } + }, + "node_modules/ethers/node_modules/tslib": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", + "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "license": "0BSD", + "peer": true + }, + "node_modules/ethers/node_modules/undici-types": { + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "license": "MIT", + "peer": true + }, + "node_modules/ethers/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -2230,6 +2393,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/siwe": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/siwe/-/siwe-3.0.0.tgz", + "integrity": "sha512-P2/ry7dHYJA6JJ5+veS//Gn2XDwNb3JMvuD6xiXX8L/PJ1SNVD4a3a8xqEbmANx+7kNQcD8YAh1B9bNKKvRy/g==", + "license": "Apache-2.0", + "dependencies": { + "@spruceid/siwe-parser": "^3.0.0", + "@stablelib/random": "^1.0.1" + }, + "peerDependencies": { + "ethers": "^5.6.8 || ^6.0.8" + } + }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index 3666086..0709c25 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", + "siwe": "^3.0.0", "viem": "^2.23.6" }, "devDependencies": { diff --git a/src/app/api/auth/nonce/route.ts b/src/app/api/auth/nonce/route.ts new file mode 100644 index 0000000..0d4bb6a --- /dev/null +++ b/src/app/api/auth/nonce/route.ts @@ -0,0 +1,10 @@ +import { NextResponse } from 'next/server'; +import { getNeynarClient } from '~/lib/neynar'; + +export async function GET() { + const client = getNeynarClient(); + + const response = await client.fetchNonce(); + + return NextResponse.json(response); +} diff --git a/src/app/api/auth/signer/route.ts b/src/app/api/auth/signer/route.ts new file mode 100644 index 0000000..9a1edd1 --- /dev/null +++ b/src/app/api/auth/signer/route.ts @@ -0,0 +1,39 @@ +import { NextResponse } from 'next/server'; +import { getNeynarClient } from '~/lib/neynar'; + +export async function GET(request: Request) { + const { searchParams } = new URL(request.url); + const message = searchParams.get('message'); + const signature = searchParams.get('signature'); + + if (!message) { + return NextResponse.json( + { error: 'Message parameter is required' }, + { status: 400 } + ); + } + + if (!signature) { + return NextResponse.json( + { error: 'Signature parameter is required' }, + { status: 400 } + ); + } + + const client = getNeynarClient(); + + let signers; + + try { + const data = await client.fetchSigners({ message, signature }); + signers = data.signers; + } catch (error) { + console.error('Error fetching signers:', error?.response?.data); + throw new Error('Failed to fetch signers'); + } + console.log('signers =>', signers); + + return NextResponse.json({ + signers, + }); +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 959cf90..90584eb 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,27 +1,38 @@ -"use client"; +'use client'; -import dynamic from "next/dynamic"; -import type { Session } from "next-auth"; -import { SessionProvider } from "next-auth/react"; -import { MiniAppProvider } from "@neynar/react"; -import { SafeFarcasterSolanaProvider } from "~/components/providers/SafeFarcasterSolanaProvider"; -import { ANALYTICS_ENABLED } from "~/lib/constants"; +import dynamic from 'next/dynamic'; +import type { Session } from 'next-auth'; +import { SessionProvider } from 'next-auth/react'; +import { MiniAppProvider } from '@neynar/react'; +import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider'; +import { ANALYTICS_ENABLED } from '~/lib/constants'; +import { AuthKitProvider } from '@farcaster/auth-kit'; const WagmiProvider = dynamic( - () => import("~/components/providers/WagmiProvider"), + () => 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"; +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} + {children} diff --git a/src/components/ui/NeynarAuthButton.tsx b/src/components/ui/NeynarAuthButton.tsx new file mode 100644 index 0000000..fbd3900 --- /dev/null +++ b/src/components/ui/NeynarAuthButton.tsx @@ -0,0 +1,526 @@ +'use client'; + +import '@farcaster/auth-kit/styles.css'; +import { useSignIn } from '@farcaster/auth-kit'; +import { useCallback, useEffect, useState, useRef } from 'react'; +import { cn } from '../../lib/utils'; +import { Button } from './Button'; + +// Utility functions for device detection +function isAndroid(): boolean { + return ( + typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent) + ); +} + +function isSmallIOS(): boolean { + return ( + typeof navigator !== 'undefined' && /iPhone|iPod/.test(navigator.userAgent) + ); +} + +function isLargeIOS(): boolean { + return ( + typeof navigator !== 'undefined' && + (/iPad/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) + ); +} + +function isIOS(): boolean { + return isSmallIOS() || isLargeIOS(); +} + +function isMobile(): boolean { + return isAndroid() || isIOS(); +} + +// Hook for detecting clicks outside an element +function useDetectClickOutside( + ref: React.RefObject, + callback: () => void +) { + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, callback]); +} + +// Storage utilities for persistence +const STORAGE_KEY = 'farcaster_auth_state'; + +interface StoredAuthState { + isAuthenticated: boolean; + userData?: { + fid?: number; + pfpUrl?: string; + username?: string; + }; + lastSignInTime?: number; +} + +function saveAuthState(state: StoredAuthState) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch (error) { + console.warn('Failed to save auth state:', error); + } +} + +function loadAuthState(): StoredAuthState | null { + try { + const stored = localStorage.getItem(STORAGE_KEY); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.warn('Failed to load auth state:', error); + return null; + } +} + +function clearAuthState() { + try { + localStorage.removeItem(STORAGE_KEY); + } catch (error) { + console.warn('Failed to clear auth state:', error); + } +} + +// QR Code Dialog Component +function QRCodeDialog({ + open, + onClose, + url, + isError, + error, +}: { + open: boolean; + onClose: () => void; + url: string; + isError: boolean; + error?: Error | null; +}) { + if (!open) return null; + + return ( +
+
+
+

+ {isError ? 'Error' : 'Sign in with Farcaster'} +

+ +
+ + {isError ? ( +
+
+ {error?.message || 'Unknown error, please try again.'} +
+ +
+ ) : ( +
+

+ To sign in with Farcaster, scan the code below with your + phone's camera. +

+ +
+
+ {/* eslint-disable-next-line @next/next/no-img-element */} + QR Code for Farcaster sign in +
+
+ + +
+ )} +
+
+ ); +} + +// Profile Button Component +function ProfileButton({ + userData, + onSignOut, +}: { + userData?: { fid?: number; pfpUrl?: string; username?: string }; + onSignOut: () => void; +}) { + const [showDropdown, setShowDropdown] = useState(false); + const ref = useRef(null); + + useDetectClickOutside(ref, () => setShowDropdown(false)); + + const name = userData?.username ?? `!${userData?.fid}`; + const pfpUrl = userData?.pfpUrl ?? 'https://farcaster.xyz/avatar.png'; + + return ( +
+ + + {showDropdown && ( +
+ +
+ )} +
+ ); +} + +// Main Custom SignInButton Component +export function NeynarAuthButton() { + const [nonce, setNonce] = useState(null); + const [showDialog, setShowDialog] = useState(false); + const [storedAuth, setStoredAuth] = useState(null); + + // Generate nonce + useEffect(() => { + const generateNonce = async () => { + try { + const response = await fetch('/api/auth/nonce'); + if (response.ok) { + const data = await response.json(); + setNonce(data.nonce); + } else { + console.error('Failed to fetch nonce'); + } + } catch (error) { + console.error('Error generating nonce:', error); + } + }; + + generateNonce(); + }, []); + + // Load stored auth state on mount + useEffect(() => { + const stored = loadAuthState(); + if (stored && stored.isAuthenticated) { + setStoredAuth(stored); + } + }, []); + + // Success callback - this is critical! + const onSuccessCallback = useCallback((res: unknown) => { + console.log('šŸŽ‰ Sign in successful!', res); + const authState: StoredAuthState = { + isAuthenticated: true, + userData: res as StoredAuthState['userData'], + lastSignInTime: Date.now(), + }; + saveAuthState(authState); + setStoredAuth(authState); + setShowDialog(false); + }, []); + + // Status response callback + const onStatusCallback = useCallback((statusData: unknown) => { + console.log('šŸ“Š Status response:', statusData); + }, []); + + // Error callback + const onErrorCallback = useCallback((error?: Error | null) => { + console.error('āŒ Sign in error:', error); + }, []); + + const signInState = useSignIn({ + nonce: nonce || undefined, + onSuccess: onSuccessCallback, + onStatusResponse: onStatusCallback, + onError: onErrorCallback, + }); + + const { + signIn, + signOut, + connect, + reconnect, + isSuccess, + isError, + error, + channelToken, + url, + data, + validSignature, + isPolling, + } = signInState; + + // Connect when component mounts and we have a nonce + useEffect(() => { + if (nonce && !channelToken) { + console.log('šŸ”Œ Connecting with nonce:', nonce); + connect(); + } + }, [nonce, channelToken, connect]); + + // Debug logging + useEffect(() => { + console.log('šŸ” Auth state:', { + isSuccess, + validSignature, + hasData: !!data, + isPolling, + isError, + storedAuth: !!storedAuth?.isAuthenticated, + }); + }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); + + // Handle fetching signers after successful authentication + useEffect(() => { + if (data?.message && data?.signature) { + console.log('šŸ“ Got message and signature:', { + message: data.message, + signature: data.signature, + }); + + const fetchSigners = async () => { + try { + const response = await fetch( + `/api/auth/signer?message=${encodeURIComponent( + data.message || '' + )}&signature=${data.signature}` + ); + + const signerData = await response.json(); + console.log('šŸ” Signer response:', signerData); + + if (response.ok) { + console.log('āœ… Signers fetched successfully:', signerData.signers); + } else { + console.error('āŒ Failed to fetch signers'); + } + } catch (error) { + console.error('āŒ Error fetching signers:', error); + } + }; + + fetchSigners(); + } + }, [data?.message, data?.signature]); + + const handleSignIn = useCallback(() => { + console.log('šŸš€ Starting sign in flow...'); + if (isError) { + console.log('šŸ”„ Reconnecting due to error...'); + reconnect(); + } + setShowDialog(true); + signIn(); + + // Open mobile app if on mobile and URL is available + if (url && isMobile()) { + console.log('šŸ“± Opening mobile app:', url); + window.open(url, '_blank'); + } + }, [isError, reconnect, signIn, url]); + + const handleSignOut = useCallback(() => { + console.log('šŸ‘‹ Signing out...'); + setShowDialog(false); + signOut(); + clearAuthState(); + setStoredAuth(null); + }, [signOut]); + + // The key fix: match the original library's authentication logic exactly + const authenticated = + (isSuccess && validSignature) || storedAuth?.isAuthenticated; + const userData = data || storedAuth?.userData; + + // Show loading state while nonce is being fetched + if (!nonce) { + return ( +
+
+
+ + Loading... + +
+
+ ); + } + + return ( + <> + {authenticated ? ( + + ) : ( + + )} + + {/* QR Code Dialog for desktop */} + {url && ( + setShowDialog(false)} + url={url} + isError={isError} + error={error} + /> + )} + + {/* Debug panel (optional - can be removed in production) */} + {/* {process.env.NODE_ENV === "development" && ( +
+
Debug Info:
+
+            {JSON.stringify(
+              {
+                authenticated,
+                isSuccess,
+                validSignature,
+                hasData: !!data,
+                isPolling,
+                isError,
+                hasStoredAuth: !!storedAuth?.isAuthenticated,
+                hasUrl: !!url,
+                hasChannelToken: !!channelToken,
+              },
+              null,
+              2
+            )}
+          
+
+ )} */} + + ); +} diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 392929c..e9c0fb7 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -1,15 +1,16 @@ -"use client"; +'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/frame-sdk"; +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/frame-sdk'; +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 @@ -17,10 +18,10 @@ import { type Haptics } from "@farcaster/frame-sdk"; * - 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 * @@ -28,63 +29,68 @@ import { type Haptics } from "@farcaster/frame-sdk"; */ export function ActionsTab() { // --- Hooks --- - const { - actions, - added, - notificationDetails, - haptics, - context, - } = useMiniApp(); - + const { actions, added, notificationDetails, haptics, context } = + useMiniApp(); + // --- State --- const [notificationState, setNotificationState] = useState({ - sendStatus: "", + sendStatus: '', shareUrlCopied: false, }); - const [selectedHapticIntensity, setSelectedHapticIntensity] = useState('medium'); + 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: "" })); + 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" }, + 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" })); + setNotificationState((prev) => ({ ...prev, sendStatus: 'Success' })); return; } else if (response.status === 429) { - setNotificationState((prev) => ({ ...prev, sendStatus: "Rate limited" })); + setNotificationState((prev) => ({ + ...prev, + sendStatus: 'Rate limited', + })); return; } const responseText = await response.text(); - setNotificationState((prev) => ({ ...prev, sendStatus: `Error: ${responseText}` })); + setNotificationState((prev) => ({ + ...prev, + sendStatus: `Error: ${responseText}`, + })); } catch (error) { - setNotificationState((prev) => ({ ...prev, sendStatus: `Error: ${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. */ @@ -93,13 +99,17 @@ export function ActionsTab() { const userShareUrl = `${process.env.NEXT_PUBLIC_URL}/share/${context.user.fid}`; await navigator.clipboard.writeText(userShareUrl); setNotificationState((prev) => ({ ...prev, shareUrlCopied: true })); - setTimeout(() => setNotificationState((prev) => ({ ...prev, shareUrlCopied: false })), 2000); + 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. */ @@ -113,56 +123,76 @@ export function ActionsTab() { // --- Render --- return ( -
+
{/* Share functionality */} - {/* Authentication */} - {/* Mini app actions */} - + {/* Neynar Authentication */} + - + + {/* Notification functionality */} {notificationState.sendStatus && ( -
+
Send notification result: {notificationState.sendStatus}
)} - {/* Share URL copying */} - {/* Haptic feedback controls */} -
-
); -} \ No newline at end of file +} From 02146b4ff58b24e099d64d20f0d78da8028a889d Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 4 Jul 2025 03:31:46 +0530 Subject: [PATCH 05/32] Remove unnecessary comment --- src/components/ui/NeynarAuthButton.tsx | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/components/ui/NeynarAuthButton.tsx b/src/components/ui/NeynarAuthButton.tsx index fbd3900..82e9307 100644 --- a/src/components/ui/NeynarAuthButton.tsx +++ b/src/components/ui/NeynarAuthButton.tsx @@ -473,13 +473,6 @@ export function NeynarAuthButton() { Initializing... ) : ( - /* The above code is a conditional rendering block in a TypeScript React component. It checks - if the environment variable `NODE_ENV` is set to "development", and if so, it renders a - debug info section displaying various boolean values related to the application state. - This debug info includes values such as `authenticated`, `isSuccess`, `validSignature`, - `hasData`, `isPolling`, `isError`, `hasStoredAuth`, `hasUrl`, and `hasChannelToken`. These - values are displayed in a formatted JSON string within a `
` element for easy
-          readability during development. */
             <>
               Sign in with Neynar
             

From 8563812b4f9172913cc6b5f4ce3f31c7038a1447 Mon Sep 17 00:00:00 2001
From: Shreyaschorge 
Date: Sat, 5 Jul 2025 00:51:31 +0530
Subject: [PATCH 06/32] Add signer creation

---
 bin/init.js                                 | 321 +++++++------
 src/app/api/auth/nonce/route.ts             |  16 +-
 src/app/api/auth/signer/route.ts            |  51 +-
 src/app/api/auth/signer/signed_key/route.ts | 101 ++++
 src/app/api/auth/signers/route.ts           |  38 ++
 src/components/ui/NeynarAuthButton.tsx      | 502 +++++++++++++++++---
 6 files changed, 804 insertions(+), 225 deletions(-)
 create mode 100644 src/app/api/auth/signer/signed_key/route.ts
 create mode 100644 src/app/api/auth/signers/route.ts

diff --git a/bin/init.js b/bin/init.js
index 3968b60..659f36c 100644
--- a/bin/init.js
+++ b/bin/init.js
@@ -12,7 +12,9 @@ const __filename = fileURLToPath(import.meta.url);
 const __dirname = dirname(__filename);
 
 const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git';
-const SCRIPT_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
+const SCRIPT_VERSION = JSON.parse(
+  fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
+).version;
 
 // ANSI color codes
 const purple = '\x1b[35m';
@@ -48,8 +50,8 @@ async function queryNeynarApp(apiKey) {
       `https://api.neynar.com/portal/app_by_api_key?starter_kit=true`,
       {
         headers: {
-          'x-api-key': apiKey
-        }
+          'x-api-key': apiKey,
+        },
       }
     );
     const data = await response.json();
@@ -80,16 +82,17 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         {
           type: 'confirm',
           name: 'useNeynar',
-          message: `🪐 ${purple}${bright}${italic}Neynar is an API that makes it easy to build on Farcaster.${reset}\n\n` +
-          'Benefits of using Neynar in your mini app:\n' +
-          '- Pre-configured webhook handling (no setup required)\n' +
-          '- Automatic mini app analytics in your dev portal\n' +
-          '- Send manual notifications from dev.neynar.com\n' +
-          '- Built-in rate limiting and error handling\n\n' +
-          `${purple}${bright}${italic}A demo API key is included if you would like to try out Neynar before signing up!${reset}\n\n` +
-          'Would you like to use Neynar in your mini app?',
-          default: true
-        }
+          message:
+            `🪐 ${purple}${bright}${italic}Neynar is an API that makes it easy to build on Farcaster.${reset}\n\n` +
+            'Benefits of using Neynar in your mini app:\n' +
+            '- Pre-configured webhook handling (no setup required)\n' +
+            '- Automatic mini app analytics in your dev portal\n' +
+            '- Send manual notifications from dev.neynar.com\n' +
+            '- Built-in rate limiting and error handling\n\n' +
+            `${purple}${bright}${italic}A demo API key is included if you would like to try out Neynar before signing up!${reset}\n\n` +
+            'Would you like to use Neynar in your mini app?',
+          default: true,
+        },
       ]);
     }
 
@@ -98,8 +101,10 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
       break;
     }
 
-    console.log('\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n');
-    
+    console.log(
+      '\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n'
+    );
+
     let neynarKeyAnswer;
     if (autoAcceptDefaults) {
       neynarKeyAnswer = { neynarApiKey: null };
@@ -109,8 +114,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
           type: 'password',
           name: 'neynarApiKey',
           message: 'Enter your Neynar API key (or press enter to skip):',
-          default: null
-        }
+          default: null,
+        },
       ]);
     }
 
@@ -126,15 +131,21 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
             type: 'confirm',
             name: 'useDemo',
             message: 'Would you like to try the demo Neynar API key?',
-            default: true
-          }
+            default: true,
+          },
         ]);
       }
 
       if (useDemoKey.useDemo) {
-        console.warn('\nāš ļø Note: the demo key is for development purposes only and is aggressively rate limited.');
-        console.log('For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.');
-        console.log(`\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`);
+        console.warn(
+          '\nāš ļø Note: the demo key is for development purposes only and is aggressively rate limited.'
+        );
+        console.log(
+          'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.'
+        );
+        console.log(
+          `\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`
+        );
         neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO';
       }
     }
@@ -144,14 +155,16 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         useNeynar = false;
         break;
       }
-      console.log('\nāš ļø  No valid API key provided. Would you like to try again?');
+      console.log(
+        '\nāš ļø  No valid API key provided. Would you like to try again?'
+      );
       const { retry } = await inquirer.prompt([
         {
           type: 'confirm',
           name: 'retry',
           message: 'Try configuring Neynar again?',
-          default: true
-        }
+          default: true,
+        },
       ]);
       if (!retry) {
         useNeynar = false;
@@ -176,9 +189,10 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         {
           type: 'confirm',
           name: 'retry',
-          message: 'āš ļø  Could not find a client ID for this API key. Would you like to try configuring Neynar again?',
-          default: true
-        }
+          message:
+            'āš ļø  Could not find a client ID for this API key. Would you like to try configuring Neynar again?',
+          default: true,
+        },
       ]);
       if (!retry) {
         useNeynar = false;
@@ -191,7 +205,10 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
     break;
   }
 
-  const defaultMiniAppName = (neynarAppName && !neynarAppName.toLowerCase().includes('demo')) ? neynarAppName : undefined;
+  const defaultMiniAppName =
+    neynarAppName && !neynarAppName.toLowerCase().includes('demo')
+      ? neynarAppName
+      : undefined;
 
   let answers;
   if (autoAcceptDefaults) {
@@ -203,7 +220,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
       buttonText: 'Launch Mini App',
       useWallet: true,
       useTunnel: true,
-      enableAnalytics: true
+      enableAnalytics: true,
     };
   } else {
     // If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options
@@ -218,21 +235,22 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
             return 'Project name cannot be empty';
           }
           return true;
-        }
-      }
+        },
+      },
     ]);
-    
+
     answers = await inquirer.prompt([
       {
         type: 'input',
         name: 'description',
         message: 'Give a one-line description of your mini app (optional):',
-        default: 'A Farcaster mini app created with Neynar'
+        default: 'A Farcaster mini app created with Neynar',
       },
       {
         type: 'list',
         name: 'primaryCategory',
-        message: 'It is strongly recommended to choose a primary category and tags to help users discover your mini app.\n\nSelect a primary category:',
+        message:
+          'It is strongly recommended to choose a primary category and tags to help users discover your mini app.\n\nSelect a primary category:',
         choices: [
           new inquirer.Separator(),
           { name: 'Skip (not recommended)', value: null },
@@ -249,23 +267,24 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
           { name: 'Education', value: 'education' },
           { name: 'Developer Tools', value: 'developer-tools' },
           { name: 'Entertainment', value: 'entertainment' },
-          { name: 'Art & Creativity', value: 'art-creativity' }
+          { name: 'Art & Creativity', value: 'art-creativity' },
         ],
-        default: null
+        default: null,
       },
       {
         type: 'input',
         name: 'tags',
-        message: 'Enter tags for your mini app (separate with spaces or commas, optional):',
+        message:
+          'Enter tags for your mini app (separate with spaces or commas, optional):',
         default: '',
         filter: (input) => {
           if (!input.trim()) return [];
           // Split by both spaces and commas, trim whitespace, and filter out empty strings
           return input
             .split(/[,\s]+/)
-            .map(tag => tag.trim())
-            .filter(tag => tag.length > 0);
-        }
+            .map((tag) => tag.trim())
+            .filter((tag) => tag.length > 0);
+        },
       },
       {
         type: 'input',
@@ -277,8 +296,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
             return 'Button text cannot be empty';
           }
           return true;
-        }
-      }
+        },
+      },
     ]);
 
     // Merge project name from the first prompt
@@ -289,7 +308,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
       {
         type: 'confirm',
         name: 'useWallet',
-        message: 'Would you like to include wallet and transaction tooling in your mini app?\n' +
+        message:
+          'Would you like to include wallet and transaction tooling in your mini app?\n' +
           'This includes:\n' +
           '- EVM wallet connection\n' +
           '- Transaction signing\n' +
@@ -297,8 +317,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
           '- Chain switching\n' +
           '- Solana support\n\n' +
           'Include wallet and transaction features?',
-        default: true
-      }
+        default: true,
+      },
     ]);
     answers.useWallet = walletAnswer.useWallet;
 
@@ -307,11 +327,12 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
       {
         type: 'confirm',
         name: 'useTunnel',
-        message: 'Would you like to test on mobile and/or test the app with Warpcast developer tools?\n' +
+        message:
+          'Would you like to test on mobile and/or test the app with Warpcast developer tools?\n' +
           `āš ļø ${yellow}${italic}Both mobile testing and the Warpcast debugger require setting up a tunnel to serve your app from localhost to the broader internet.\n${reset}` +
           'Configure a tunnel for mobile testing and/or Warpcast developer tools?',
-        default: true
-      }
+        default: true,
+      },
     ]);
     answers.useTunnel = hostingAnswer.useTunnel;
 
@@ -320,9 +341,10 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
       {
         type: 'confirm',
         name: 'enableAnalytics',
-        message: 'Would you like to help improve Neynar products by sharing usage data from your mini app?',
-        default: true
-      }
+        message:
+          'Would you like to help improve Neynar products by sharing usage data from your mini app?',
+        default: true,
+      },
     ]);
     answers.enableAnalytics = analyticsAnswer.enableAnalytics;
   }
@@ -337,19 +359,19 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
   try {
     console.log(`\nCloning repository from ${REPO_URL}...`);
     // Use separate commands for better cross-platform compatibility
-    execSync(`git clone ${REPO_URL} "${projectPath}"`, { 
+    execSync(`git clone ${REPO_URL} "${projectPath}"`, {
       stdio: 'inherit',
-      shell: process.platform === 'win32'
+      shell: process.platform === 'win32',
     });
-    execSync('git fetch origin main', { 
-      cwd: projectPath, 
+    execSync('git fetch origin main', {
+      cwd: projectPath,
       stdio: 'inherit',
-      shell: process.platform === 'win32'
+      shell: process.platform === 'win32',
     });
-    execSync('git reset --hard origin/main', { 
-      cwd: projectPath, 
+    execSync('git reset --hard origin/main', {
+      cwd: projectPath,
       stdio: 'inherit',
-      shell: process.platform === 'win32'
+      shell: process.platform === 'win32',
     });
   } catch (error) {
     console.error('\nāŒ Error: Failed to create project directory.');
@@ -386,47 +408,48 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
 
   // Add dependencies
   packageJson.dependencies = {
-    "@farcaster/auth-client": ">=0.3.0 <1.0.0",
-    "@farcaster/auth-kit": ">=0.6.0 <1.0.0",
-    "@farcaster/frame-core": ">=0.0.29 <1.0.0",
-    "@farcaster/frame-node": ">=0.0.18 <1.0.0",
-    "@farcaster/frame-sdk": ">=0.0.31 <1.0.0",
-    "@farcaster/frame-wagmi-connector": ">=0.0.19 <1.0.0",
-    "@farcaster/mini-app-solana": ">=0.0.17 <1.0.0",
-    "@neynar/react": "^1.2.5",
-    "@radix-ui/react-label": "^2.1.1",
-    "@solana/wallet-adapter-react": "^0.15.38",
-    "@tanstack/react-query": "^5.61.0",
-    "@upstash/redis": "^1.34.3",
-    "class-variance-authority": "^0.7.1",
-    "clsx": "^2.1.1",
-    "dotenv": "^16.4.7",
-    "lucide-react": "^0.469.0",
-    "mipd": "^0.0.7",
-    "next": "^15",
-    "next-auth": "^4.24.11",
-    "react": "^19",
-    "react-dom": "^19",
-    "tailwind-merge": "^2.6.0",
-    "tailwindcss-animate": "^1.0.7",
-    "viem": "^2.23.6",
-    "wagmi": "^2.14.12",
-    "zod": "^3.24.2"
+    '@farcaster/auth-client': '>=0.3.0 <1.0.0',
+    '@farcaster/auth-kit': '>=0.6.0 <1.0.0',
+    '@farcaster/frame-core': '>=0.0.29 <1.0.0',
+    '@farcaster/frame-node': '>=0.0.18 <1.0.0',
+    '@farcaster/frame-sdk': '>=0.0.31 <1.0.0',
+    '@farcaster/frame-wagmi-connector': '>=0.0.19 <1.0.0',
+    '@farcaster/mini-app-solana': '>=0.0.17 <1.0.0',
+    '@neynar/react': '^1.2.5',
+    '@radix-ui/react-label': '^2.1.1',
+    '@solana/wallet-adapter-react': '^0.15.38',
+    '@tanstack/react-query': '^5.61.0',
+    '@upstash/redis': '^1.34.3',
+    'class-variance-authority': '^0.7.1',
+    clsx: '^2.1.1',
+    dotenv: '^16.4.7',
+    'lucide-react': '^0.469.0',
+    mipd: '^0.0.7',
+    next: '^15',
+    'next-auth': '^4.24.11',
+    react: '^19',
+    'react-dom': '^19',
+    'tailwind-merge': '^2.6.0',
+    'tailwindcss-animate': '^1.0.7',
+    viem: '^2.23.6',
+    wagmi: '^2.14.12',
+    zod: '^3.24.2',
+    siwe: '^3.0.0',
   };
 
   packageJson.devDependencies = {
-    "@types/node": "^20",
-    "@types/react": "^19",
-    "@types/react-dom": "^19",
-    "@vercel/sdk": "^1.9.0",
-    "crypto": "^1.0.1",
-    "eslint": "^8",
-    "eslint-config-next": "15.0.3",
-    "localtunnel": "^2.0.2",
-    "pino-pretty": "^13.0.0",
-    "postcss": "^8",
-    "tailwindcss": "^3.4.1",
-    "typescript": "^5"
+    '@types/node': '^20',
+    '@types/react': '^19',
+    '@types/react-dom': '^19',
+    '@vercel/sdk': '^1.9.0',
+    crypto: '^1.0.1',
+    eslint: '^8',
+    'eslint-config-next': '15.0.3',
+    localtunnel: '^2.0.2',
+    'pino-pretty': '^13.0.0',
+    postcss: '^8',
+    tailwindcss: '^3.4.1',
+    typescript: '^5',
   };
 
   // Add Neynar SDK if selected
@@ -452,35 +475,46 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
     const constantsPath = path.join(projectPath, 'src', 'lib', 'constants.ts');
     if (fs.existsSync(constantsPath)) {
       let constantsContent = fs.readFileSync(constantsPath, 'utf8');
-      
+
       // Helper function to escape single quotes in strings
       const escapeString = (str) => str.replace(/'/g, "\\'");
-      
+
       // Helper function to safely replace constants with validation
       const safeReplace = (content, pattern, replacement, constantName) => {
         const match = content.match(pattern);
         if (!match) {
-          console.log(`āš ļø  Warning: Could not update ${constantName} in constants.ts. Pattern not found.`);
+          console.log(
+            `āš ļø  Warning: Could not update ${constantName} in constants.ts. Pattern not found.`
+          );
           console.log(`Pattern: ${pattern}`);
-          console.log(`Expected to match in: ${content.split('\n').find(line => line.includes(constantName)) || 'Not found'}`);
+          console.log(
+            `Expected to match in: ${
+              content.split('\n').find((line) => line.includes(constantName)) ||
+              'Not found'
+            }`
+          );
         } else {
           const newContent = content.replace(pattern, replacement);
           return newContent;
         }
         return content;
       };
-      
+
       // Regex patterns that match whole lines with export const
       const patterns = {
         APP_NAME: /^export const APP_NAME\s*=\s*['"`][^'"`]*['"`];$/m,
-        APP_DESCRIPTION: /^export const APP_DESCRIPTION\s*=\s*['"`][^'"`]*['"`];$/m,
-        APP_PRIMARY_CATEGORY: /^export const APP_PRIMARY_CATEGORY\s*=\s*['"`][^'"`]*['"`];$/m,
+        APP_DESCRIPTION:
+          /^export const APP_DESCRIPTION\s*=\s*['"`][^'"`]*['"`];$/m,
+        APP_PRIMARY_CATEGORY:
+          /^export const APP_PRIMARY_CATEGORY\s*=\s*['"`][^'"`]*['"`];$/m,
         APP_TAGS: /^export const APP_TAGS\s*=\s*\[[^\]]*\];$/m,
-        APP_BUTTON_TEXT: /^export const APP_BUTTON_TEXT\s*=\s*['"`][^'"`]*['"`];$/m,
+        APP_BUTTON_TEXT:
+          /^export const APP_BUTTON_TEXT\s*=\s*['"`][^'"`]*['"`];$/m,
         USE_WALLET: /^export const USE_WALLET\s*=\s*(true|false);$/m,
-        ANALYTICS_ENABLED: /^export const ANALYTICS_ENABLED\s*=\s*(true|false);$/m
+        ANALYTICS_ENABLED:
+          /^export const ANALYTICS_ENABLED\s*=\s*(true|false);$/m,
       };
-      
+
       // Update APP_NAME
       constantsContent = safeReplace(
         constantsContent,
@@ -488,42 +522,49 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         `export const APP_NAME = '${escapeString(answers.projectName)}';`,
         'APP_NAME'
       );
-      
+
       // Update APP_DESCRIPTION
       constantsContent = safeReplace(
         constantsContent,
         patterns.APP_DESCRIPTION,
-        `export const APP_DESCRIPTION = '${escapeString(answers.description)}';`,
+        `export const APP_DESCRIPTION = '${escapeString(
+          answers.description
+        )}';`,
         'APP_DESCRIPTION'
       );
-      
+
       // Update APP_PRIMARY_CATEGORY (always update, null becomes empty string)
       constantsContent = safeReplace(
         constantsContent,
         patterns.APP_PRIMARY_CATEGORY,
-        `export const APP_PRIMARY_CATEGORY = '${escapeString(answers.primaryCategory || '')}';`,
+        `export const APP_PRIMARY_CATEGORY = '${escapeString(
+          answers.primaryCategory || ''
+        )}';`,
         'APP_PRIMARY_CATEGORY'
       );
-      
+
       // Update APP_TAGS
-      const tagsString = answers.tags.length > 0 
-        ? `['${answers.tags.map(tag => escapeString(tag)).join("', '")}']` 
-        : "['neynar', 'starter-kit', 'demo']";
+      const tagsString =
+        answers.tags.length > 0
+          ? `['${answers.tags.map((tag) => escapeString(tag)).join("', '")}']`
+          : "['neynar', 'starter-kit', 'demo']";
       constantsContent = safeReplace(
         constantsContent,
         patterns.APP_TAGS,
         `export const APP_TAGS = ${tagsString};`,
         'APP_TAGS'
       );
-      
+
       // Update APP_BUTTON_TEXT (always update, use answers value)
       constantsContent = safeReplace(
         constantsContent,
         patterns.APP_BUTTON_TEXT,
-        `export const APP_BUTTON_TEXT = '${escapeString(answers.buttonText || '')}';`,
+        `export const APP_BUTTON_TEXT = '${escapeString(
+          answers.buttonText || ''
+        )}';`,
         'APP_BUTTON_TEXT'
       );
-      
+
       // Update USE_WALLET
       constantsContent = safeReplace(
         constantsContent,
@@ -531,7 +572,7 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         `export const USE_WALLET = ${answers.useWallet};`,
         'USE_WALLET'
       );
-      
+
       // Update ANALYTICS_ENABLED
       constantsContent = safeReplace(
         constantsContent,
@@ -539,24 +580,31 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
         `export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`,
         'ANALYTICS_ENABLED'
       );
-      
+
       fs.writeFileSync(constantsPath, constantsContent);
     } else {
       console.log('āš ļø  constants.ts not found, skipping constants update');
     }
 
-    fs.appendFileSync(envPath, `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`);
+    fs.appendFileSync(
+      envPath,
+      `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
+    );
     if (useNeynar && neynarApiKey && neynarClientId) {
       fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
       fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
     } else if (useNeynar) {
-      console.log('\nāš ļø  Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID');
+      console.log(
+        '\nāš ļø  Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID'
+      );
     }
     fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
-    
+
     fs.unlinkSync(envExamplePath);
   } else {
-    console.log('\n.env.example does not exist, skipping copy and remove operations');
+    console.log(
+      '\n.env.example does not exist, skipping copy and remove operations'
+    );
   }
 
   // Update README
@@ -564,7 +612,9 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
   const readmePath = path.join(projectPath, 'README.md');
   const prependText = `\n\n`;
   if (fs.existsSync(readmePath)) {
-    const originalReadmeContent = fs.readFileSync(readmePath, { encoding: 'utf8' });
+    const originalReadmeContent = fs.readFileSync(readmePath, {
+      encoding: 'utf8',
+    });
     const updatedReadmeContent = prependText + originalReadmeContent;
     fs.writeFileSync(readmePath, updatedReadmeContent);
   } else {
@@ -574,15 +624,15 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
   // Install dependencies
   console.log('\nInstalling dependencies...');
 
-  execSync('npm cache clean --force', { 
-    cwd: projectPath, 
+  execSync('npm cache clean --force', {
+    cwd: projectPath,
     stdio: 'inherit',
-    shell: process.platform === 'win32'
+    shell: process.platform === 'win32',
   });
-  execSync('npm install', { 
-    cwd: projectPath, 
+  execSync('npm install', {
+    cwd: projectPath,
     stdio: 'inherit',
-    shell: process.platform === 'win32'
+    shell: process.platform === 'win32',
   });
 
   // Remove the bin directory
@@ -596,12 +646,15 @@ export async function init(projectName = null, autoAcceptDefaults = false) {
   console.log('\nInitializing git repository...');
   execSync('git init', { cwd: projectPath });
   execSync('git add .', { cwd: projectPath });
-  execSync('git commit -m "initial commit from @neynar/create-farcaster-mini-app"', { cwd: projectPath });
+  execSync(
+    'git commit -m "initial commit from @neynar/create-farcaster-mini-app"',
+    { cwd: projectPath }
+  );
 
   // Calculate border length based on message length
   const message = `✨🪐 Successfully created mini app ${finalProjectName} with git and dependencies installed! 🪐✨`;
   const borderLength = message.length;
-  const borderStars = '✨'.repeat((borderLength / 2) + 1);
+  const borderStars = '✨'.repeat(borderLength / 2 + 1);
 
   console.log(`\n${borderStars}`);
   console.log(`${message}`);
diff --git a/src/app/api/auth/nonce/route.ts b/src/app/api/auth/nonce/route.ts
index 0d4bb6a..a1f25ea 100644
--- a/src/app/api/auth/nonce/route.ts
+++ b/src/app/api/auth/nonce/route.ts
@@ -2,9 +2,15 @@ import { NextResponse } from 'next/server';
 import { getNeynarClient } from '~/lib/neynar';
 
 export async function GET() {
-  const client = getNeynarClient();
-
-  const response = await client.fetchNonce();
-
-  return NextResponse.json(response);
+  try {
+    const client = getNeynarClient();
+    const response = await client.fetchNonce();
+    return NextResponse.json(response);
+  } catch (error) {
+    console.error('Error fetching nonce:', error);
+    return NextResponse.json(
+      { error: 'Failed to fetch nonce' },
+      { status: 500 }
+    );
+  }
 }
diff --git a/src/app/api/auth/signer/route.ts b/src/app/api/auth/signer/route.ts
index 9a1edd1..f793d0e 100644
--- a/src/app/api/auth/signer/route.ts
+++ b/src/app/api/auth/signer/route.ts
@@ -1,39 +1,42 @@
 import { NextResponse } from 'next/server';
 import { getNeynarClient } from '~/lib/neynar';
 
+export async function POST() {
+  try {
+    const neynarClient = getNeynarClient();
+    const signer = await neynarClient.createSigner();
+    return NextResponse.json(signer);
+  } catch (error) {
+    console.error('Error fetching signer:', error);
+    return NextResponse.json(
+      { error: 'Failed to fetch signer' },
+      { status: 500 }
+    );
+  }
+}
+
 export async function GET(request: Request) {
   const { searchParams } = new URL(request.url);
-  const message = searchParams.get('message');
-  const signature = searchParams.get('signature');
+  const signerUuid = searchParams.get('signerUuid');
 
-  if (!message) {
+  if (!signerUuid) {
     return NextResponse.json(
-      { error: 'Message parameter is required' },
+      { error: 'signerUuid is required' },
       { status: 400 }
     );
   }
 
-  if (!signature) {
-    return NextResponse.json(
-      { error: 'Signature parameter is required' },
-      { status: 400 }
-    );
-  }
-
-  const client = getNeynarClient();
-
-  let signers;
-
   try {
-    const data = await client.fetchSigners({ message, signature });
-    signers = data.signers;
+    const neynarClient = getNeynarClient();
+    const signer = await neynarClient.lookupSigner({
+      signerUuid,
+    });
+    return NextResponse.json(signer);
   } catch (error) {
-    console.error('Error fetching signers:', error?.response?.data);
-    throw new Error('Failed to fetch signers');
+    console.error('Error fetching signed key:', error);
+    return NextResponse.json(
+      { error: 'Failed to fetch signed key' },
+      { status: 500 }
+    );
   }
-  console.log('signers =>', signers);
-
-  return NextResponse.json({
-    signers,
-  });
 }
diff --git a/src/app/api/auth/signer/signed_key/route.ts b/src/app/api/auth/signer/signed_key/route.ts
new file mode 100644
index 0000000..de4c414
--- /dev/null
+++ b/src/app/api/auth/signer/signed_key/route.ts
@@ -0,0 +1,101 @@
+import { NextResponse } from 'next/server';
+import { getNeynarClient } from '~/lib/neynar';
+import { mnemonicToAccount } from 'viem/accounts';
+
+const postRequiredFields = ['signerUuid', 'publicKey'];
+
+const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {
+  name: 'Farcaster SignedKeyRequestValidator',
+  version: '1',
+  chainId: 10,
+  verifyingContract:
+    '0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`,
+};
+
+const SIGNED_KEY_REQUEST_TYPE = [
+  { name: 'requestFid', type: 'uint256' },
+  { name: 'key', type: 'bytes' },
+  { name: 'deadline', type: 'uint256' },
+];
+
+export async function POST(request: Request) {
+  const body = await request.json();
+
+  // Validate required fields
+  for (const field of postRequiredFields) {
+    if (!body[field]) {
+      return NextResponse.json(
+        { error: `${field} is required` },
+        { status: 400 }
+      );
+    }
+  }
+
+  const { signerUuid, publicKey, redirectUrl } = body;
+
+  if (redirectUrl && typeof redirectUrl !== 'string') {
+    return NextResponse.json(
+      { error: 'redirectUrl must be a string' },
+      { status: 400 }
+    );
+  }
+
+  try {
+    // Get the app's account from seed phrase
+    const seedPhrase = process.env.SEED_PHRASE;
+    const shouldSponsor = process.env.SPONSOR_SIGNER === 'true';
+
+    if (!seedPhrase) {
+      return NextResponse.json(
+        { error: 'App configuration missing (SEED_PHRASE or FID)' },
+        { status: 500 }
+      );
+    }
+
+    const neynarClient = getNeynarClient();
+
+    const account = mnemonicToAccount(seedPhrase);
+
+    const {
+      user: { fid },
+    } = await neynarClient.lookupUserByCustodyAddress({
+      custodyAddress: account.address,
+    });
+
+    const appFid = fid;
+
+    // Generate deadline (24 hours from now)
+    const deadline = Math.floor(Date.now() / 1000) + 86400;
+
+    // Generate EIP-712 signature
+    const signature = await account.signTypedData({
+      domain: SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
+      types: {
+        SignedKeyRequest: SIGNED_KEY_REQUEST_TYPE,
+      },
+      primaryType: 'SignedKeyRequest',
+      message: {
+        requestFid: BigInt(appFid),
+        key: publicKey,
+        deadline: BigInt(deadline),
+      },
+    });
+
+    const signer = await neynarClient.registerSignedKey({
+      appFid,
+      deadline,
+      signature,
+      signerUuid,
+      ...(redirectUrl && { redirectUrl }),
+      ...(shouldSponsor && { sponsor: { sponsored_by_neynar: true } }),
+    });
+
+    return NextResponse.json(signer);
+  } catch (error) {
+    console.error('Error registering signed key:', error);
+    return NextResponse.json(
+      { error: 'Failed to register signed key' },
+      { status: 500 }
+    );
+  }
+}
diff --git a/src/app/api/auth/signers/route.ts b/src/app/api/auth/signers/route.ts
new file mode 100644
index 0000000..1c89acf
--- /dev/null
+++ b/src/app/api/auth/signers/route.ts
@@ -0,0 +1,38 @@
+import { NextResponse } from 'next/server';
+import { getNeynarClient } from '~/lib/neynar';
+
+const requiredParams = ['message', 'signature'];
+
+export async function GET(request: Request) {
+  const { searchParams } = new URL(request.url);
+  const params: Record = {};
+  for (const param of requiredParams) {
+    params[param] = searchParams.get(param);
+    if (!params[param]) {
+      return NextResponse.json(
+        {
+          error: `${param} parameter is required`,
+        },
+        { status: 400 }
+      );
+    }
+  }
+
+  const message = params.message as string;
+  const signature = params.signature as string;
+
+  try {
+    const client = getNeynarClient();
+    const data = await client.fetchSigners({ message, signature });
+    const signers = data.signers;
+    return NextResponse.json({
+      signers,
+    });
+  } catch (error) {
+    console.error('Error fetching signers:', error);
+    return NextResponse.json(
+      { error: 'Failed to fetch signers' },
+      { status: 500 }
+    );
+  }
+}
diff --git a/src/components/ui/NeynarAuthButton.tsx b/src/components/ui/NeynarAuthButton.tsx
index 82e9307..7a1afad 100644
--- a/src/components/ui/NeynarAuthButton.tsx
+++ b/src/components/ui/NeynarAuthButton.tsx
@@ -64,6 +64,13 @@ interface StoredAuthState {
     username?: string;
   };
   lastSignInTime?: number;
+  signers?: {
+    object: 'signer';
+    signer_uuid: string;
+    public_key: string;
+    status: 'approved';
+    fid: number;
+  }[]; // Store the list of signers
 }
 
 function saveAuthState(state: StoredAuthState) {
@@ -92,28 +99,156 @@ function clearAuthState() {
   }
 }
 
-// QR Code Dialog Component
-function QRCodeDialog({
+function updateSignersInAuthState(
+  signers: StoredAuthState['signers']
+): StoredAuthState | null {
+  try {
+    const stored = loadAuthState();
+    if (stored) {
+      const updatedState = { ...stored, signers };
+      saveAuthState(updatedState);
+      return updatedState;
+    }
+  } catch (error) {
+    console.warn('Failed to update signers in auth state:', error);
+  }
+  return null;
+}
+
+export function getStoredSigners(): unknown[] {
+  try {
+    const stored = loadAuthState();
+    return stored?.signers || [];
+  } catch (error) {
+    console.warn('Failed to get stored signers:', error);
+    return [];
+  }
+}
+
+// Enhanced QR Code Dialog Component with multiple steps
+function AuthDialog({
   open,
   onClose,
   url,
   isError,
   error,
+  step,
+  isLoading,
+  signerApprovalUrl,
 }: {
   open: boolean;
   onClose: () => void;
   url: string;
   isError: boolean;
   error?: Error | null;
+  step: 'signin' | 'access' | 'loading';
+  isLoading?: boolean;
+  signerApprovalUrl?: string | null;
 }) {
   if (!open) return null;
 
+  const getStepContent = () => {
+    switch (step) {
+      case 'signin':
+        return {
+          title: 'Signin',
+          description:
+            "To signin, scan the code below with your phone's camera.",
+          showQR: true,
+          qrUrl: url,
+          showOpenButton: true,
+        };
+
+      case 'loading':
+        return {
+          title: 'Setting up access...',
+          description:
+            'Checking your account permissions and setting up secure access.',
+          showQR: false,
+          qrUrl: '',
+          showOpenButton: false,
+        };
+
+      case 'access':
+        return {
+          title: 'Grant Access',
+          description: (
+            
+

+ Allow this app to access your Farcaster account: +

+
+
+
+ + + +
+
+
+ Read Access +
+
+ View your profile and public information +
+
+
+
+
+ + + +
+
+
+ Write Access +
+
+ Post casts, likes, and update your profile +
+
+
+
+
+ ), + // Show QR code if we have signer approval URL, otherwise show loading + showQR: !!signerApprovalUrl, + qrUrl: signerApprovalUrl || '', + showOpenButton: !!signerApprovalUrl, + }; + + default: + return { + title: 'Sign in', + description: + "To signin, scan the code below with your phone's camera.", + showQR: true, + qrUrl: url, + showOpenButton: true, + }; + } + }; + + const content = getStepContent(); + return (
-
+

- {isError ? 'Error' : 'Sign in with Farcaster'} + {isError ? 'Error' : content.title}

+ + + + + I'm using my phone → + + )}
)}
@@ -281,8 +438,176 @@ function ProfileButton({ // Main Custom SignInButton Component export function NeynarAuthButton() { const [nonce, setNonce] = useState(null); - const [showDialog, setShowDialog] = useState(false); const [storedAuth, setStoredAuth] = useState(null); + const [signersLoading, setSignersLoading] = useState(false); + + // New state for unified dialog flow + const [showDialog, setShowDialog] = useState(false); + const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( + 'loading' + ); + const [signerApprovalUrl, setSignerApprovalUrl] = useState( + null + ); + const [pollingInterval, setPollingInterval] = useState( + null + ); + + // Helper function to create a signer + const createSigner = useCallback(async () => { + try { + console.log('šŸ”§ Creating new signer...'); + + const response = await fetch('/api/auth/signer', { + method: 'POST', + }); + + if (!response.ok) { + throw new Error('Failed to create signer'); + } + + const signerData = await response.json(); + console.log('āœ… Signer created:', signerData); + + return signerData; + } catch (error) { + console.error('āŒ Error creating signer:', error); + throw error; + } + }, []); + + // Helper function to generate signed key request + const generateSignedKeyRequest = useCallback( + async (signerUuid: string, publicKey: string) => { + try { + console.log('šŸ”‘ Generating signed key request...'); + + // Prepare request body + const requestBody: { + signerUuid: string; + publicKey: string; + sponsor?: { sponsored_by_neynar: boolean }; + } = { + signerUuid, + publicKey, + }; + + const response = await fetch('/api/auth/signer/signed_key', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `Failed to generate signed key request: ${errorData.error}` + ); + } + + const data = await response.json(); + console.log('āœ… Signed key request generated:', data); + + return data; + } catch (error) { + console.error('āŒ Error generating signed key request:', error); + throw error; + } + }, + [] + ); + + // Helper function to fetch all signers + const fetchAllSigners = useCallback( + async (message: string, signature: string) => { + try { + console.log('ļæ½ Fetching all signers...'); + setSignersLoading(true); + + const response = await fetch( + `/api/auth/signers?message=${encodeURIComponent( + message + )}&signature=${signature}` + ); + + const signerData = await response.json(); + console.log('ļæ½ Signer response:', signerData); + + if (response.ok) { + console.log('āœ… Signers fetched successfully:', signerData.signers); + + // Store signers in localStorage + const updatedState = updateSignersInAuthState( + signerData.signers || [] + ); + if (updatedState) { + setStoredAuth(updatedState); + } + + return signerData.signers; + } else { + console.error('āŒ Failed to fetch signers'); + throw new Error('Failed to fetch signers'); + } + } catch (error) { + console.error('āŒ Error fetching signers:', error); + throw error; + } finally { + setSignersLoading(false); + } + }, + [] + ); + + // Helper function to poll signer status + const startPolling = useCallback( + (signerUuid: string, message: string, signature: string) => { + console.log('ļæ½ Starting polling for signer:', signerUuid); + + const interval = setInterval(async () => { + try { + const response = await fetch( + `/api/auth/signer?signerUuid=${signerUuid}` + ); + + if (!response.ok) { + throw new Error('Failed to poll signer status'); + } + + const signerData = await response.json(); + console.log('ļæ½ Signer status:', signerData.status); + + if (signerData.status === 'approved') { + console.log('šŸŽ‰ Signer approved!'); + clearInterval(interval); + setPollingInterval(null); + setShowDialog(false); + setDialogStep('signin'); + setSignerApprovalUrl(null); + + // Refetch all signers + await fetchAllSigners(message, signature); + } + } catch (error) { + console.error('āŒ Error polling signer:', error); + } + }, 1000); // Poll every 1 second + + setPollingInterval(interval); + }, + [fetchAllSigners] + ); + + // Cleanup polling on unmount + useEffect(() => { + return () => { + if (pollingInterval) { + clearInterval(pollingInterval); + } + }; + }, [pollingInterval]); // Generate nonce useEffect(() => { @@ -308,6 +633,9 @@ export function NeynarAuthButton() { const stored = loadAuthState(); if (stored && stored.isAuthenticated) { setStoredAuth(stored); + if (stored.signers && stored.signers.length > 0) { + console.log('šŸ“‚ Loaded stored signers:', stored.signers); + } } }, []); @@ -321,7 +649,7 @@ export function NeynarAuthButton() { }; saveAuthState(authState); setStoredAuth(authState); - setShowDialog(false); + // setShowDialog(false); }, []); // Status response callback @@ -383,31 +711,68 @@ export function NeynarAuthButton() { message: data.message, signature: data.signature, }); - - const fetchSigners = async () => { + const handleSignerFlow = async () => { try { - const response = await fetch( - `/api/auth/signer?message=${encodeURIComponent( - data.message || '' - )}&signature=${data.signature}` - ); + // Ensure we have message and signature + if (!data.message || !data.signature) { + console.error('āŒ Missing message or signature'); + return; + } - const signerData = await response.json(); - console.log('šŸ” Signer response:', signerData); + // Step 1: Change to loading state + setDialogStep('loading'); + setSignersLoading(true); - if (response.ok) { - console.log('āœ… Signers fetched successfully:', signerData.signers); + // First, fetch existing signers + const signers = await fetchAllSigners(data.message, data.signature); + + // Check if no signers exist + if (!signers || signers.length === 0) { + console.log('ļæ½ No signers found, creating new signer...'); + + // Step 1: Create a signer + const newSigner = await createSigner(); + + // Step 2: Generate signed key request + const signedKeyData = await generateSignedKeyRequest( + newSigner.signer_uuid, + newSigner.public_key + ); + + // Step 3: Show QR code in access dialog for signer approval + if (signedKeyData.signer_approval_url) { + setSignerApprovalUrl(signedKeyData.signer_approval_url); + setSignersLoading(false); // Stop loading, show QR code + setDialogStep('access'); // Switch to access step to show QR + + // Step 4: Start polling for signer approval + startPolling(newSigner.signer_uuid, data.message, data.signature); + } } else { - console.error('āŒ Failed to fetch signers'); + // If signers exist, close the dialog + console.log('āœ… Signers already exist, closing dialog'); + setSignersLoading(false); + setShowDialog(false); + setDialogStep('signin'); } } catch (error) { - console.error('āŒ Error fetching signers:', error); + console.error('āŒ Error in signer flow:', error); + // On error, reset to signin step + setDialogStep('signin'); + setSignersLoading(false); } }; - fetchSigners(); + handleSignerFlow(); } - }, [data?.message, data?.signature]); + }, [ + data?.message, + data?.signature, + fetchAllSigners, + createSigner, + generateSignedKeyRequest, + startPolling, + ]); const handleSignIn = useCallback(() => { console.log('šŸš€ Starting sign in flow...'); @@ -415,6 +780,7 @@ export function NeynarAuthButton() { console.log('šŸ”„ Reconnecting due to error...'); reconnect(); } + setDialogStep('signin'); setShowDialog(true); signIn(); @@ -435,11 +801,12 @@ export function NeynarAuthButton() { // The key fix: match the original library's authentication logic exactly const authenticated = - (isSuccess && validSignature) || storedAuth?.isAuthenticated; + ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && + !!(storedAuth?.signers && storedAuth.signers.length > 0); const userData = data || storedAuth?.userData; - // Show loading state while nonce is being fetched - if (!nonce) { + // Show loading state while nonce is being fetched or signers are loading + if (!nonce || signersLoading) { return (
@@ -463,7 +830,7 @@ export function NeynarAuthButton() { className={cn( 'btn btn-primary flex items-center gap-3', 'disabled:opacity-50 disabled:cursor-not-allowed', - 'transform transition-all duration-200 hover:scale-[1.02] active:scale-[0.98]', + 'transform transition-all duration-200 active:scale-[0.98]', !url && 'cursor-not-allowed' )} > @@ -480,14 +847,25 @@ export function NeynarAuthButton() { )} - {/* QR Code Dialog for desktop */} + {/* Unified Auth Dialog */} {url && ( - setShowDialog(false)} + onClose={() => { + setShowDialog(false); + setDialogStep('signin'); + setSignerApprovalUrl(null); + if (pollingInterval) { + clearInterval(pollingInterval); + setPollingInterval(null); + } + }} url={url} isError={isError} error={error} + step={dialogStep} + isLoading={signersLoading} + signerApprovalUrl={signerApprovalUrl} /> )} From f92becd789aae16102a8234748f455e2e49a7127 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Sat, 5 Jul 2025 00:58:57 +0530 Subject: [PATCH 07/32] Comment console.logs --- src/components/ui/NeynarAuthButton.tsx | 74 ++++++++++++-------------- 1 file changed, 34 insertions(+), 40 deletions(-) diff --git a/src/components/ui/NeynarAuthButton.tsx b/src/components/ui/NeynarAuthButton.tsx index 7a1afad..417c6c1 100644 --- a/src/components/ui/NeynarAuthButton.tsx +++ b/src/components/ui/NeynarAuthButton.tsx @@ -456,7 +456,7 @@ export function NeynarAuthButton() { // Helper function to create a signer const createSigner = useCallback(async () => { try { - console.log('šŸ”§ Creating new signer...'); + // console.log('šŸ”§ Creating new signer...'); const response = await fetch('/api/auth/signer', { method: 'POST', @@ -467,11 +467,11 @@ export function NeynarAuthButton() { } const signerData = await response.json(); - console.log('āœ… Signer created:', signerData); + // console.log('āœ… Signer created:', signerData); return signerData; } catch (error) { - console.error('āŒ Error creating signer:', error); + // console.error('āŒ Error creating signer:', error); throw error; } }, []); @@ -480,7 +480,7 @@ export function NeynarAuthButton() { const generateSignedKeyRequest = useCallback( async (signerUuid: string, publicKey: string) => { try { - console.log('šŸ”‘ Generating signed key request...'); + // console.log('šŸ”‘ Generating signed key request...'); // Prepare request body const requestBody: { @@ -508,7 +508,7 @@ export function NeynarAuthButton() { } const data = await response.json(); - console.log('āœ… Signed key request generated:', data); + // console.log('āœ… Signed key request generated:', data); return data; } catch (error) { @@ -523,7 +523,7 @@ export function NeynarAuthButton() { const fetchAllSigners = useCallback( async (message: string, signature: string) => { try { - console.log('ļæ½ Fetching all signers...'); + // console.log('ļæ½ Fetching all signers...'); setSignersLoading(true); const response = await fetch( @@ -533,10 +533,10 @@ export function NeynarAuthButton() { ); const signerData = await response.json(); - console.log('ļæ½ Signer response:', signerData); + // console.log('ļæ½ Signer response:', signerData); if (response.ok) { - console.log('āœ… Signers fetched successfully:', signerData.signers); + // console.log('āœ… Signers fetched successfully:', signerData.signers); // Store signers in localStorage const updatedState = updateSignersInAuthState( @@ -564,7 +564,7 @@ export function NeynarAuthButton() { // Helper function to poll signer status const startPolling = useCallback( (signerUuid: string, message: string, signature: string) => { - console.log('ļæ½ Starting polling for signer:', signerUuid); + // console.log('ļæ½ Starting polling for signer:', signerUuid); const interval = setInterval(async () => { try { @@ -577,10 +577,10 @@ export function NeynarAuthButton() { } const signerData = await response.json(); - console.log('ļæ½ Signer status:', signerData.status); + // console.log('ļæ½ Signer status:', signerData.status); if (signerData.status === 'approved') { - console.log('šŸŽ‰ Signer approved!'); + // console.log('šŸŽ‰ Signer approved!'); clearInterval(interval); setPollingInterval(null); setShowDialog(false); @@ -634,14 +634,14 @@ export function NeynarAuthButton() { if (stored && stored.isAuthenticated) { setStoredAuth(stored); if (stored.signers && stored.signers.length > 0) { - console.log('šŸ“‚ Loaded stored signers:', stored.signers); + // console.log('šŸ“‚ Loaded stored signers:', stored.signers); } } }, []); // Success callback - this is critical! const onSuccessCallback = useCallback((res: unknown) => { - console.log('šŸŽ‰ Sign in successful!', res); + // console.log('šŸŽ‰ Sign in successful!', res); const authState: StoredAuthState = { isAuthenticated: true, userData: res as StoredAuthState['userData'], @@ -652,11 +652,6 @@ export function NeynarAuthButton() { // setShowDialog(false); }, []); - // Status response callback - const onStatusCallback = useCallback((statusData: unknown) => { - console.log('šŸ“Š Status response:', statusData); - }, []); - // Error callback const onErrorCallback = useCallback((error?: Error | null) => { console.error('āŒ Sign in error:', error); @@ -665,7 +660,6 @@ export function NeynarAuthButton() { const signInState = useSignIn({ nonce: nonce || undefined, onSuccess: onSuccessCallback, - onStatusResponse: onStatusCallback, onError: onErrorCallback, }); @@ -687,30 +681,30 @@ export function NeynarAuthButton() { // Connect when component mounts and we have a nonce useEffect(() => { if (nonce && !channelToken) { - console.log('šŸ”Œ Connecting with nonce:', nonce); + // console.log('šŸ”Œ Connecting with nonce:', nonce); connect(); } }, [nonce, channelToken, connect]); // Debug logging - useEffect(() => { - console.log('šŸ” Auth state:', { - isSuccess, - validSignature, - hasData: !!data, - isPolling, - isError, - storedAuth: !!storedAuth?.isAuthenticated, - }); - }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); + // useEffect(() => { + // console.log('šŸ” Auth state:', { + // isSuccess, + // validSignature, + // hasData: !!data, + // isPolling, + // isError, + // storedAuth: !!storedAuth?.isAuthenticated, + // }); + // }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); // Handle fetching signers after successful authentication useEffect(() => { if (data?.message && data?.signature) { - console.log('šŸ“ Got message and signature:', { - message: data.message, - signature: data.signature, - }); + // console.log('šŸ“ Got message and signature:', { + // message: data.message, + // signature: data.signature, + // }); const handleSignerFlow = async () => { try { // Ensure we have message and signature @@ -728,7 +722,7 @@ export function NeynarAuthButton() { // Check if no signers exist if (!signers || signers.length === 0) { - console.log('ļæ½ No signers found, creating new signer...'); + // console.log('ļæ½ No signers found, creating new signer...'); // Step 1: Create a signer const newSigner = await createSigner(); @@ -750,7 +744,7 @@ export function NeynarAuthButton() { } } else { // If signers exist, close the dialog - console.log('āœ… Signers already exist, closing dialog'); + // console.log('āœ… Signers already exist, closing dialog'); setSignersLoading(false); setShowDialog(false); setDialogStep('signin'); @@ -775,9 +769,9 @@ export function NeynarAuthButton() { ]); const handleSignIn = useCallback(() => { - console.log('šŸš€ Starting sign in flow...'); + // console.log('šŸš€ Starting sign in flow...'); if (isError) { - console.log('šŸ”„ Reconnecting due to error...'); + // console.log('šŸ”„ Reconnecting due to error...'); reconnect(); } setDialogStep('signin'); @@ -786,13 +780,13 @@ export function NeynarAuthButton() { // Open mobile app if on mobile and URL is available if (url && isMobile()) { - console.log('šŸ“± Opening mobile app:', url); + // console.log('šŸ“± Opening mobile app:', url); window.open(url, '_blank'); } }, [isError, reconnect, signIn, url]); const handleSignOut = useCallback(() => { - console.log('šŸ‘‹ Signing out...'); + // console.log('šŸ‘‹ Signing out...'); setShowDialog(false); signOut(); clearAuthState(); From 4884ac402d1f2c256acf118d5688660ae961fe04 Mon Sep 17 00:00:00 2001 From: Manan Date: Mon, 7 Jul 2025 00:35:46 -0700 Subject: [PATCH 08/32] feat: change how accountAssociation is collected --- scripts/build.js | 359 ++++++++++-------------- scripts/deploy.js | 698 ++++++++++++++++++++++------------------------ src/lib/utils.ts | 86 ++---- 3 files changed, 509 insertions(+), 634 deletions(-) diff --git a/scripts/build.js b/scripts/build.js index b680399..6d6bccb 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,111 +1,74 @@ -import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import { mnemonicToAccount } from 'viem/accounts'; -import { fileURLToPath } from 'url'; -import inquirer from 'inquirer'; -import dotenv from 'dotenv'; -import crypto from 'crypto'; - -// ANSI color codes -const yellow = '\x1b[33m'; -const italic = '\x1b[3m'; -const reset = '\x1b[0m'; +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import inquirer from "inquirer"; +import dotenv from "dotenv"; +import crypto from "crypto"; // Load environment variables in specific order // First load .env for main config -dotenv.config({ path: '.env' }); - -async function lookupFidByCustodyAddress(custodyAddress, apiKey) { - if (!apiKey) { - throw new Error('Neynar API key is required'); - } - const lowerCasedCustodyAddress = custodyAddress.toLowerCase(); - - const response = await fetch( - `https://api.neynar.com/v2/farcaster/user/bulk-by-address?addresses=${lowerCasedCustodyAddress}&address_types=custody_address`, - { - headers: { - 'accept': 'application/json', - 'x-api-key': 'FARCASTER_V2_FRAMES_DEMO' - } - } - ); - - if (!response.ok) { - throw new Error(`Failed to lookup FID: ${response.statusText}`); - } - - const data = await response.json(); - if (!data[lowerCasedCustodyAddress]?.length || !data[lowerCasedCustodyAddress][0].custody_address) { - throw new Error('No FID found for this custody address'); - } - - return data[lowerCasedCustodyAddress][0].fid; -} +dotenv.config({ path: ".env" }); async function loadEnvLocal() { try { - if (fs.existsSync('.env.local')) { + if (fs.existsSync(".env.local")) { const { loadLocal } = await inquirer.prompt([ { - type: 'confirm', - name: 'loadLocal', - message: 'Found .env.local, likely created by the install script - would you like to load its values?', - default: false - } + type: "confirm", + name: "loadLocal", + message: + "Found .env.local, likely created by the install script - would you like to load its values?", + default: false, + }, ]); if (loadLocal) { - console.log('Loading values from .env.local...'); - const localEnv = dotenv.parse(fs.readFileSync('.env.local')); - - // Copy all values except SEED_PHRASE to .env - const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; + console.log("Loading values from .env.local..."); + const localEnv = dotenv.parse(fs.readFileSync(".env.local")); + + // Copy all values to .env + const envContent = fs.existsSync(".env") + ? fs.readFileSync(".env", "utf8") + "\n" + : ""; let newEnvContent = envContent; - + for (const [key, value] of Object.entries(localEnv)) { - if (key !== 'SEED_PHRASE') { - // Update process.env - process.env[key] = value; - // Add to .env content if not already there - if (!envContent.includes(`${key}=`)) { - newEnvContent += `${key}="${value}"\n`; - } + // Update process.env + process.env[key] = value; + // Add to .env content if not already there + if (!envContent.includes(`${key}=`)) { + newEnvContent += `${key}="${value}"\n`; } } - - // Write updated content to .env - fs.writeFileSync('.env', newEnvContent); - console.log('āœ… Values from .env.local have been written to .env'); - } - } - // Always try to load SEED_PHRASE from .env.local - if (fs.existsSync('.env.local')) { - const localEnv = dotenv.parse(fs.readFileSync('.env.local')); - if (localEnv.SEED_PHRASE) { - process.env.SEED_PHRASE = localEnv.SEED_PHRASE; + // Write updated content to .env + fs.writeFileSync(".env", newEnvContent); + console.log("āœ… Values from .env.local have been written to .env"); } } } catch (error) { // Error reading .env.local, which is fine - console.log('Note: No .env.local file found'); + console.log("Note: No .env.local file found"); } } // TODO: make sure rebuilding is supported const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.join(__dirname, '..'); +const projectRoot = path.join(__dirname, ".."); async function validateDomain(domain) { // Remove http:// or https:// if present - const cleanDomain = domain.replace(/^https?:\/\//, ''); - + const cleanDomain = domain.replace(/^https?:\/\//, ""); + // Basic domain validation - if (!cleanDomain.match(/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/)) { - throw new Error('Invalid domain format'); + if ( + !cleanDomain.match( + /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/ + ) + ) { + throw new Error("Invalid domain format"); } return cleanDomain; @@ -120,54 +83,26 @@ async function queryNeynarApp(apiKey) { `https://api.neynar.com/portal/app_by_api_key`, { headers: { - 'x-api-key': apiKey - } + "x-api-key": apiKey, + }, } ); const data = await response.json(); return data; } catch (error) { - console.error('Error querying Neynar app data:', error); + console.error("Error querying Neynar app data:", error); return null; } } -async function validateSeedPhrase(seedPhrase) { - try { - // Try to create an account from the seed phrase - const account = mnemonicToAccount(seedPhrase); - return account.address; - } catch (error) { - throw new Error('Invalid seed phrase'); - } -} - -async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl) { - const header = { - type: 'custody', - key: accountAddress, - fid, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const account = mnemonicToAccount(seedPhrase); - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); +async function generateFarcasterMetadata(domain, webhookUrl) { + const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(","); return { accountAssociation: { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature + header: "", + payload: "", + signature: "", }, frame: { version: "1", @@ -188,18 +123,19 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase async function main() { try { - console.log('\nšŸ“ Checking environment variables...'); - console.log('Loading values from .env...'); - + console.log("\nšŸ“ Checking environment variables..."); + console.log("Loading values from .env..."); + // Load .env.local if user wants to await loadEnvLocal(); // Get domain from user const { domain } = await inquirer.prompt([ { - type: 'input', - name: 'domain', - message: 'Enter the domain where your mini app will be deployed (e.g., example.com):', + type: "input", + name: "domain", + message: + "Enter the domain where your mini app will be deployed (e.g., example.com):", validate: async (input) => { try { await validateDomain(input); @@ -207,40 +143,41 @@ async function main() { } catch (error) { return error.message; } - } - } + }, + }, ]); // Get frame name from user const { frameName } = await inquirer.prompt([ { - type: 'input', - name: 'frameName', - message: 'Enter the name for your mini app (e.g., My Cool Mini App):', + type: "input", + name: "frameName", + message: "Enter the name for your mini app (e.g., My Cool Mini App):", default: process.env.NEXT_PUBLIC_MINI_APP_NAME, validate: (input) => { - if (input.trim() === '') { - return 'Mini app name cannot be empty'; + if (input.trim() === "") { + return "Mini app name cannot be empty"; } return true; - } - } + }, + }, ]); // Get button text from user const { buttonText } = await inquirer.prompt([ { - type: 'input', - name: 'buttonText', - message: 'Enter the text for your mini app button:', - default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App', + type: "input", + name: "buttonText", + message: "Enter the text for your mini app button:", + default: + process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || "Launch Mini App", validate: (input) => { - if (input.trim() === '') { - return 'Button text cannot be empty'; + if (input.trim() === "") { + return "Button text cannot be empty"; } return true; - } - } + }, + }, ]); // Get Neynar configuration @@ -252,15 +189,16 @@ async function main() { if (!neynarApiKey) { const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([ { - type: 'password', - name: 'neynarApiKey', - message: 'Enter your Neynar API key (optional - leave blank to skip):', - default: null - } + type: "password", + name: "neynarApiKey", + message: + "Enter your Neynar API key (optional - leave blank to skip):", + default: null, + }, ]); neynarApiKey = inputNeynarApiKey; } else { - console.log('Using existing Neynar API key from .env'); + console.log("Using existing Neynar API key from .env"); } if (!neynarApiKey) { @@ -273,7 +211,7 @@ async function main() { const appInfo = await queryNeynarApp(neynarApiKey); if (appInfo) { neynarClientId = appInfo.app_uuid; - console.log('āœ… Fetched Neynar app client ID'); + console.log("āœ… Fetched Neynar app client ID"); break; } } @@ -284,14 +222,16 @@ async function main() { } // If we get here, the API key was invalid - console.log('\nāš ļø Could not find Neynar app information. The API key may be incorrect.'); + console.log( + "\nāš ļø Could not find Neynar app information. The API key may be incorrect." + ); const { retry } = await inquirer.prompt([ { - type: 'confirm', - name: 'retry', - message: 'Would you like to try a different API key?', - default: true - } + type: "confirm", + name: "retry", + message: "Would you like to try a different API key?", + default: true, + }, ]); // Reset for retry @@ -304,51 +244,23 @@ async function main() { } } - // Get seed phrase from user - let seedPhrase = process.env.SEED_PHRASE; - if (!seedPhrase) { - const { seedPhrase: inputSeedPhrase } = await inquirer.prompt([ - { - type: 'password', - name: 'seedPhrase', - message: 'Your farcaster custody account seed phrase is required to create a signature proving this app was created by you.\n' + - `āš ļø ${yellow}${italic}seed phrase is only used to sign the mini app manifest, then discarded${reset} āš ļø\n` + - 'Seed phrase:', - validate: async (input) => { - try { - await validateSeedPhrase(input); - return true; - } catch (error) { - return error.message; - } - } - } - ]); - seedPhrase = inputSeedPhrase; - } else { - console.log('Using existing seed phrase from .env'); - } + // Generate manifest + console.log("\nšŸ”Ø Generating mini app manifest..."); - // Validate seed phrase and get account address - const accountAddress = await validateSeedPhrase(seedPhrase); - console.log('āœ… Generated account address from seed phrase'); - - const fid = await lookupFidByCustodyAddress(accountAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO'); - - // Generate and sign manifest - console.log('\nšŸ”Ø Generating mini app manifest...'); - // Determine webhook URL based on environment variables - const webhookUrl = neynarApiKey && neynarClientId - ? `https://api.neynar.com/f/app/${neynarClientId}/event` - : `${domain}/api/webhook`; + const webhookUrl = + neynarApiKey && neynarClientId + ? `https://api.neynar.com/f/app/${neynarClientId}/event` + : `https://${domain}/api/webhook`; - const metadata = await generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl); - console.log('\nāœ… Mini app manifest generated' + (seedPhrase ? ' and signed' : '')); + const metadata = await generateFarcasterMetadata(domain, webhookUrl); + console.log("\nāœ… Mini app manifest generated"); // Read existing .env file or create new one - const envPath = path.join(projectRoot, '.env'); - let envContent = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''; + const envPath = path.join(projectRoot, ".env"); + let envContent = fs.existsSync(envPath) + ? fs.readFileSync(envPath, "utf8") + : ""; // Add or update environment variables const newEnvVars = [ @@ -357,26 +269,38 @@ async function main() { // Mini app metadata `NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`, - `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || ''}"`, - `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || ''}"`, - `NEXT_PUBLIC_MINI_APP_TAGS="${process.env.NEXT_PUBLIC_MINI_APP_TAGS || ''}"`, + `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${ + process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || "" + }"`, + `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${ + process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || "" + }"`, + `NEXT_PUBLIC_MINI_APP_TAGS="${ + process.env.NEXT_PUBLIC_MINI_APP_TAGS || "" + }"`, `NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`, // Analytics - `NEXT_PUBLIC_ANALYTICS_ENABLED="${process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'}"`, + `NEXT_PUBLIC_ANALYTICS_ENABLED="${ + process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || "false" + }"`, // Neynar configuration (if it exists in current env) - ...(process.env.NEYNAR_API_KEY ? - [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), - ...(neynarClientId ? - [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), + ...(process.env.NEYNAR_API_KEY + ? [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] + : []), + ...(neynarClientId ? [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), // FID (if it exists in current env) ...(process.env.FID ? [`FID="${process.env.FID}"`] : []), - `NEXT_PUBLIC_USE_WALLET="${process.env.NEXT_PUBLIC_USE_WALLET || 'false'}"`, + `NEXT_PUBLIC_USE_WALLET="${ + process.env.NEXT_PUBLIC_USE_WALLET || "false" + }"`, // NextAuth configuration - `NEXTAUTH_SECRET="${process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex')}"`, + `NEXTAUTH_SECRET="${ + process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex") + }"`, `NEXTAUTH_URL="https://${domain}"`, // Mini app manifest with signature @@ -384,14 +308,14 @@ async function main() { ]; // Filter out empty values and join with newlines - const validEnvVars = newEnvVars.filter(line => { - const [, value] = line.split('='); + const validEnvVars = newEnvVars.filter((line) => { + const [, value] = line.split("="); return value && value !== '""'; }); // Update or append each environment variable - validEnvVars.forEach(varLine => { - const [key] = varLine.split('='); + validEnvVars.forEach((varLine) => { + const [key] = varLine.split("="); if (envContent.includes(`${key}=`)) { envContent = envContent.replace(new RegExp(`${key}=.*`), varLine); } else { @@ -402,22 +326,27 @@ async function main() { // Write updated .env file fs.writeFileSync(envPath, envContent); - console.log('\nāœ… Environment variables updated'); + console.log("\nāœ… Environment variables updated"); // Run next build - console.log('\nBuilding Next.js application...'); - const nextBin = path.normalize(path.join(projectRoot, 'node_modules', '.bin', 'next')); - execSync(`"${nextBin}" build`, { - cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32' + console.log("\nBuilding Next.js application..."); + const nextBin = path.normalize( + path.join(projectRoot, "node_modules", ".bin", "next") + ); + execSync(`"${nextBin}" build`, { + cwd: projectRoot, + stdio: "inherit", + shell: process.platform === "win32", }); - console.log('\n✨ Build complete! Your mini app is ready for deployment. 🪐'); - console.log('šŸ“ Make sure to configure the environment variables from .env in your hosting provider'); - + console.log( + "\n✨ Build complete! Your mini app is ready for deployment. 🪐" + ); + console.log( + "šŸ“ Make sure to configure the environment variables from .env in your hosting provider" + ); } catch (error) { - console.error('\nāŒ Error:', error.message); + console.error("\nāŒ Error:", error.message); process.exit(1); } } diff --git a/scripts/deploy.js b/scripts/deploy.js index ca53f97..dbb0dcb 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -1,85 +1,24 @@ -import { execSync, spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { fileURLToPath } from 'url'; -import inquirer from 'inquirer'; -import dotenv from 'dotenv'; -import crypto from 'crypto'; -import { mnemonicToAccount } from 'viem/accounts'; -import { Vercel } from '@vercel/sdk'; +import { execSync, spawn } from "child_process"; +import fs from "fs"; +import path from "path"; +import os from "os"; +import { fileURLToPath } from "url"; +import inquirer from "inquirer"; +import dotenv from "dotenv"; +import crypto from "crypto"; +import { Vercel } from "@vercel/sdk"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.join(__dirname, '..'); +const projectRoot = path.join(__dirname, ".."); // Load environment variables in specific order -dotenv.config({ path: '.env' }); +dotenv.config({ path: ".env" }); -async function validateSeedPhrase(seedPhrase) { - try { - const account = mnemonicToAccount(seedPhrase); - return account.address; - } catch (error) { - throw new Error('Invalid seed phrase'); - } -} - -async function lookupFidByCustodyAddress(custodyAddress, apiKey) { - if (!apiKey) { - throw new Error('Neynar API key is required'); - } - const lowerCasedCustodyAddress = custodyAddress.toLowerCase(); - - const response = await fetch( - `https://api.neynar.com/v2/farcaster/user/bulk-by-address?addresses=${lowerCasedCustodyAddress}&address_types=custody_address`, - { - headers: { - 'accept': 'application/json', - 'x-api-key': apiKey - } - } - ); - - if (!response.ok) { - throw new Error(`Failed to lookup FID: ${response.statusText}`); - } - - const data = await response.json(); - if (!data[lowerCasedCustodyAddress]?.length || !data[lowerCasedCustodyAddress][0].custody_address) { - throw new Error('No FID found for this custody address'); - } - - return data[lowerCasedCustodyAddress][0].fid; -} - -async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase, webhookUrl) { +async function generateFarcasterMetadata(domain, webhookUrl) { const trimmedDomain = domain.trim(); - const header = { - type: 'custody', - key: accountAddress, - fid, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain: trimmedDomain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const account = mnemonicToAccount(seedPhrase); - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); + const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(","); return { - accountAssociation: { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature - }, frame: { version: "1", name: process.env.NEXT_PUBLIC_MINI_APP_NAME, @@ -99,130 +38,107 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase async function loadEnvLocal() { try { - if (fs.existsSync('.env.local')) { + if (fs.existsSync(".env.local")) { const { loadLocal } = await inquirer.prompt([ { - type: 'confirm', - name: 'loadLocal', - message: 'Found .env.local - would you like to load its values in addition to .env values? (except for SEED_PHRASE, values will be written to .env)', - default: true - } + type: "confirm", + name: "loadLocal", + message: + "Found .env.local - would you like to load its values in addition to .env values?", + default: true, + }, ]); if (loadLocal) { - console.log('Loading values from .env.local...'); - const localEnv = dotenv.parse(fs.readFileSync('.env.local')); - + console.log("Loading values from .env.local..."); + const localEnv = dotenv.parse(fs.readFileSync(".env.local")); + const allowedVars = [ - 'SEED_PHRASE', - 'NEXT_PUBLIC_MINI_APP_NAME', - 'NEXT_PUBLIC_MINI_APP_DESCRIPTION', - 'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY', - 'NEXT_PUBLIC_MINI_APP_TAGS', - 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', - 'NEXT_PUBLIC_ANALYTICS_ENABLED', - 'NEYNAR_API_KEY', - 'NEYNAR_CLIENT_ID' + "NEXT_PUBLIC_MINI_APP_NAME", + "NEXT_PUBLIC_MINI_APP_DESCRIPTION", + "NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY", + "NEXT_PUBLIC_MINI_APP_TAGS", + "NEXT_PUBLIC_MINI_APP_BUTTON_TEXT", + "NEXT_PUBLIC_ANALYTICS_ENABLED", + "NEYNAR_API_KEY", + "NEYNAR_CLIENT_ID", ]; - - const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') + '\n' : ''; + + const envContent = fs.existsSync(".env") + ? fs.readFileSync(".env", "utf8") + "\n" + : ""; let newEnvContent = envContent; - + for (const [key, value] of Object.entries(localEnv)) { if (allowedVars.includes(key)) { process.env[key] = value; - if (key !== 'SEED_PHRASE' && !envContent.includes(`${key}=`)) { + if (!envContent.includes(`${key}=`)) { newEnvContent += `${key}="${value}"\n`; } } } - - fs.writeFileSync('.env', newEnvContent); - console.log('āœ… Values from .env.local have been written to .env'); + + fs.writeFileSync(".env", newEnvContent); + console.log("āœ… Values from .env.local have been written to .env"); } } } catch (error) { - console.log('Note: No .env.local file found'); + console.log("Note: No .env.local file found"); } } async function checkRequiredEnvVars() { - console.log('\nšŸ“ Checking environment variables...'); - console.log('Loading values from .env...'); - + console.log("\nšŸ“ Checking environment variables..."); + console.log("Loading values from .env..."); + await loadEnvLocal(); const requiredVars = [ { - name: 'NEXT_PUBLIC_MINI_APP_NAME', - message: 'Enter the name for your frame (e.g., My Cool Mini App):', + name: "NEXT_PUBLIC_MINI_APP_NAME", + message: "Enter the name for your frame (e.g., My Cool Mini App):", default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: input => input.trim() !== '' || 'Mini app name cannot be empty' + validate: (input) => + input.trim() !== "" || "Mini app name cannot be empty", }, { - name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', - message: 'Enter the text for your frame button:', - default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', - validate: input => input.trim() !== '' || 'Button text cannot be empty' - } + name: "NEXT_PUBLIC_MINI_APP_BUTTON_TEXT", + message: "Enter the text for your frame button:", + default: + process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? "Launch Mini App", + validate: (input) => input.trim() !== "" || "Button text cannot be empty", + }, ]; - const missingVars = requiredVars.filter(varConfig => !process.env[varConfig.name]); - + const missingVars = requiredVars.filter( + (varConfig) => !process.env[varConfig.name] + ); + if (missingVars.length > 0) { - console.log('\nāš ļø Some required information is missing. Let\'s set it up:'); + console.log("\nāš ļø Some required information is missing. Let's set it up:"); for (const varConfig of missingVars) { const { value } = await inquirer.prompt([ { - type: 'input', - name: 'value', + type: "input", + name: "value", message: varConfig.message, default: varConfig.default, - validate: varConfig.validate - } + validate: varConfig.validate, + }, ]); - + process.env[varConfig.name] = value; - - const envContent = fs.existsSync('.env') ? fs.readFileSync('.env', 'utf8') : ''; - + + const envContent = fs.existsSync(".env") + ? fs.readFileSync(".env", "utf8") + : ""; + if (!envContent.includes(`${varConfig.name}=`)) { - const newLine = envContent ? '\n' : ''; - fs.appendFileSync('.env', `${newLine}${varConfig.name}="${value.trim()}"`); - } - } - } - - // Check for seed phrase - if (!process.env.SEED_PHRASE) { - console.log('\nšŸ”‘ Mini App Manifest Signing'); - console.log('A signed manifest helps users trust your mini app.'); - const { seedPhrase } = await inquirer.prompt([ - { - type: 'password', - name: 'seedPhrase', - message: 'Enter your Farcaster custody account seed phrase to sign the mini app manifest\n(optional -- leave blank to create an unsigned mini app)\n\nSeed phrase:', - default: null - } - ]); - - if (seedPhrase) { - process.env.SEED_PHRASE = seedPhrase; - - const { storeSeedPhrase } = await inquirer.prompt([ - { - type: 'confirm', - name: 'storeSeedPhrase', - message: 'Would you like to store this seed phrase in .env.local for future use?', - default: false - } - ]); - - if (storeSeedPhrase) { - fs.appendFileSync('.env.local', `\nSEED_PHRASE="${seedPhrase}"`); - console.log('āœ… Seed phrase stored in .env.local'); - } else { - console.log('ā„¹ļø Seed phrase will only be used for this deployment'); + const newLine = envContent ? "\n" : ""; + fs.appendFileSync( + ".env", + `${newLine}${varConfig.name}="${value.trim()}"` + ); } } } @@ -230,9 +146,9 @@ async function checkRequiredEnvVars() { async function getGitRemote() { try { - const remoteUrl = execSync('git remote get-url origin', { + const remoteUrl = execSync("git remote get-url origin", { cwd: projectRoot, - encoding: 'utf8' + encoding: "utf8", }).trim(); return remoteUrl; } catch (error) { @@ -242,9 +158,9 @@ async function getGitRemote() { async function checkVercelCLI() { try { - execSync('vercel --version', { - stdio: 'ignore', - shell: process.platform === 'win32' + execSync("vercel --version", { + stdio: "ignore", + shell: process.platform === "win32", }); return true; } catch (error) { @@ -253,95 +169,101 @@ async function checkVercelCLI() { } async function installVercelCLI() { - console.log('Installing Vercel CLI...'); - execSync('npm install -g vercel', { - stdio: 'inherit', - shell: process.platform === 'win32' + console.log("Installing Vercel CLI..."); + execSync("npm install -g vercel", { + stdio: "inherit", + shell: process.platform === "win32", }); } async function getVercelToken() { try { // Try to get token from Vercel CLI config - const configPath = path.join(os.homedir(), '.vercel', 'auth.json'); + const configPath = path.join(os.homedir(), ".vercel", "auth.json"); if (fs.existsSync(configPath)) { - const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); + const authConfig = JSON.parse(fs.readFileSync(configPath, "utf8")); return authConfig.token; } } catch (error) { - console.warn('Could not read Vercel token from config file'); + console.warn("Could not read Vercel token from config file"); } - + // Try environment variable if (process.env.VERCEL_TOKEN) { return process.env.VERCEL_TOKEN; } - + // Try to extract from vercel whoami try { - const whoamiOutput = execSync('vercel whoami', { - encoding: 'utf8', - stdio: 'pipe' + const whoamiOutput = execSync("vercel whoami", { + encoding: "utf8", + stdio: "pipe", }); - + // If we can get whoami, we're logged in, but we need the actual token // The token isn't directly exposed, so we'll need to use CLI for some operations - console.log('āœ… Verified Vercel CLI authentication'); + console.log("āœ… Verified Vercel CLI authentication"); return null; // We'll fall back to CLI operations } catch (error) { - throw new Error('Not logged in to Vercel CLI. Please run this script again to login.'); + throw new Error( + "Not logged in to Vercel CLI. Please run this script again to login." + ); } } async function loginToVercel() { - console.log('\nšŸ”‘ Vercel Login'); - console.log('You can either:'); - console.log('1. Log in to an existing Vercel account'); - console.log('2. Create a new Vercel account during login\n'); - console.log('If creating a new account:'); + console.log("\nšŸ”‘ Vercel Login"); + console.log("You can either:"); + console.log("1. Log in to an existing Vercel account"); + console.log("2. Create a new Vercel account during login\n"); + console.log("If creating a new account:"); console.log('1. Click "Continue with GitHub"'); - console.log('2. Authorize GitHub access'); - console.log('3. Complete the Vercel account setup in your browser'); - console.log('4. Return here once your Vercel account is created\n'); - console.log('\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account'); - - const child = spawn('vercel', ['login'], { - stdio: 'inherit' + console.log("2. Authorize GitHub access"); + console.log("3. Complete the Vercel account setup in your browser"); + console.log("4. Return here once your Vercel account is created\n"); + console.log( + "\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account" + ); + + const child = spawn("vercel", ["login"], { + stdio: "inherit", }); await new Promise((resolve, reject) => { - child.on('close', (code) => { + child.on("close", (code) => { resolve(); }); }); - console.log('\nšŸ“± Waiting for login to complete...'); - console.log('If you\'re creating a new account, please complete the Vercel account setup in your browser first.'); - + console.log("\nšŸ“± Waiting for login to complete..."); + console.log( + "If you're creating a new account, please complete the Vercel account setup in your browser first." + ); + for (let i = 0; i < 150; i++) { try { - execSync('vercel whoami', { stdio: 'ignore' }); - console.log('āœ… Successfully logged in to Vercel!'); + execSync("vercel whoami", { stdio: "ignore" }); + console.log("āœ… Successfully logged in to Vercel!"); return true; } catch (error) { - if (error.message.includes('Account not found')) { - console.log('ā„¹ļø Waiting for Vercel account setup to complete...'); + if (error.message.includes("Account not found")) { + console.log("ā„¹ļø Waiting for Vercel account setup to complete..."); } - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); } } - console.error('\nāŒ Login timed out. Please ensure you have:'); - console.error('1. Completed the Vercel account setup in your browser'); - console.error('2. Authorized the GitHub integration'); - console.error('Then try running this script again.'); + console.error("\nāŒ Login timed out. Please ensure you have:"); + console.error("1. Completed the Vercel account setup in your browser"); + console.error("2. Authorized the GitHub integration"); + console.error("Then try running this script again."); return false; } async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { try { let processedValue; - if (typeof value === 'object') { + if (typeof value === "object") { processedValue = JSON.stringify(value); } else { processedValue = value.toString(); @@ -349,11 +271,11 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { // Get existing environment variables const existingVars = await vercelClient.projects.getEnvironmentVariables({ - idOrName: projectId + idOrName: projectId, }); - const existingVar = existingVars.envs?.find(env => - env.key === key && env.target?.includes('production') + const existingVar = existingVars.envs?.find( + (env) => env.key === key && env.target?.includes("production") ); if (existingVar) { @@ -363,8 +285,8 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { id: existingVar.id, requestBody: { value: processedValue, - target: ['production'] - } + target: ["production"], + }, }); console.log(`āœ… Updated environment variable: ${key}`); } else { @@ -374,16 +296,19 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { requestBody: { key: key, value: processedValue, - type: 'encrypted', - target: ['production'] - } + type: "encrypted", + target: ["production"], + }, }); console.log(`āœ… Created environment variable: ${key}`); } - + return true; } catch (error) { - console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + console.warn( + `āš ļø Warning: Failed to set environment variable ${key}:`, + error.message + ); return false; } } @@ -394,15 +319,15 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { try { execSync(`vercel env rm ${key} production -y`, { cwd: projectRoot, - stdio: 'ignore', - env: process.env + stdio: "ignore", + env: process.env, }); } catch (error) { // Ignore errors from removal } let processedValue; - if (typeof value === 'object') { + if (typeof value === "object") { processedValue = JSON.stringify(value); } else { processedValue = value.toString(); @@ -410,11 +335,11 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { // Create temporary file const tempFilePath = path.join(projectRoot, `${key}_temp.txt`); - fs.writeFileSync(tempFilePath, processedValue, 'utf8'); + fs.writeFileSync(tempFilePath, processedValue, "utf8"); // Use appropriate command based on platform let command; - if (process.platform === 'win32') { + if (process.platform === "win32") { command = `type "${tempFilePath}" | vercel env add ${key} production`; } else { command = `cat "${tempFilePath}" | vercel env add ${key} production`; @@ -422,9 +347,9 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { execSync(command, { cwd: projectRoot, - stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts + stdio: "pipe", // Changed from 'inherit' to avoid interactive prompts shell: true, - env: process.env + env: process.env, }); fs.unlinkSync(tempFilePath); @@ -435,72 +360,95 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } - console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + console.warn( + `āš ļø Warning: Failed to set environment variable ${key}:`, + error.message + ); return false; } } -async function setEnvironmentVariables(vercelClient, projectId, envVars, projectRoot) { - console.log('\nšŸ“ Setting up environment variables...'); - +async function setEnvironmentVariables( + vercelClient, + projectId, + envVars, + projectRoot +) { + console.log("\nšŸ“ Setting up environment variables..."); + const results = []; - + for (const [key, value] of Object.entries(envVars)) { if (!value) continue; - + let success = false; - + // Try SDK approach first if we have a Vercel client if (vercelClient && projectId) { success = await setVercelEnvVarSDK(vercelClient, projectId, key, value); } - + // Fallback to CLI approach if (!success) { success = await setVercelEnvVarCLI(key, value, projectRoot); } - + results.push({ key, success }); } - + // Report results - const failed = results.filter(r => !r.success); + const failed = results.filter((r) => !r.success); if (failed.length > 0) { console.warn(`\nāš ļø Failed to set ${failed.length} environment variables:`); - failed.forEach(r => console.warn(` - ${r.key}`)); - console.warn('\nYou may need to set these manually in the Vercel dashboard.'); + failed.forEach((r) => console.warn(` - ${r.key}`)); + console.warn( + "\nYou may need to set these manually in the Vercel dashboard." + ); } - + return results; } async function deployToVercel(useGitHub = false) { try { - console.log('\nšŸš€ Deploying to Vercel...'); - + console.log("\nšŸš€ Deploying to Vercel..."); + // Ensure vercel.json exists - const vercelConfigPath = path.join(projectRoot, 'vercel.json'); + const vercelConfigPath = path.join(projectRoot, "vercel.json"); if (!fs.existsSync(vercelConfigPath)) { - console.log('šŸ“ Creating vercel.json configuration...'); - fs.writeFileSync(vercelConfigPath, JSON.stringify({ - buildCommand: "next build", - framework: "nextjs" - }, null, 2)); + console.log("šŸ“ Creating vercel.json configuration..."); + fs.writeFileSync( + vercelConfigPath, + JSON.stringify( + { + buildCommand: "next build", + framework: "nextjs", + }, + null, + 2 + ) + ); } // Set up Vercel project - console.log('\nšŸ“¦ Setting up Vercel project...'); - console.log('An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n'); - console.log('\nāš ļø Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n'); - - execSync('vercel', { + console.log("\nšŸ“¦ Setting up Vercel project..."); + console.log( + "An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n" + ); + console.log( + "\nāš ļø Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n" + ); + + execSync("vercel", { cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32' + stdio: "inherit", + shell: process.platform === "win32", }); // Load project info - const projectJson = JSON.parse(fs.readFileSync('.vercel/project.json', 'utf8')); + const projectJson = JSON.parse( + fs.readFileSync(".vercel/project.json", "utf8") + ); const projectId = projectJson.projectId; // Get Vercel token and initialize SDK client @@ -509,192 +457,219 @@ async function deployToVercel(useGitHub = false) { const token = await getVercelToken(); if (token) { vercelClient = new Vercel({ - bearerToken: token + bearerToken: token, }); - console.log('āœ… Initialized Vercel SDK client'); + console.log("āœ… Initialized Vercel SDK client"); } } catch (error) { - console.warn('āš ļø Could not initialize Vercel SDK, falling back to CLI operations'); + console.warn( + "āš ļø Could not initialize Vercel SDK, falling back to CLI operations" + ); } // Get project details - console.log('\nšŸ” Getting project details...'); + console.log("\nšŸ” Getting project details..."); let domain; let projectName; if (vercelClient) { try { const project = await vercelClient.projects.get({ - idOrName: projectId + idOrName: projectId, }); projectName = project.name; domain = `${projectName}.vercel.app`; - console.log('🌐 Using project name for domain:', domain); + console.log("🌐 Using project name for domain:", domain); } catch (error) { - console.warn('āš ļø Could not get project details via SDK, using CLI fallback'); + console.warn( + "āš ļø Could not get project details via SDK, using CLI fallback" + ); } } // Fallback to CLI method if SDK failed if (!domain) { - const inspectOutput = execSync(`vercel project inspect ${projectId} 2>&1`, { - cwd: projectRoot, - encoding: 'utf8' - }); + const inspectOutput = execSync( + `vercel project inspect ${projectId} 2>&1`, + { + cwd: projectRoot, + encoding: "utf8", + } + ); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); if (nameMatch) { projectName = nameMatch[1].trim(); domain = `${projectName}.vercel.app`; - console.log('🌐 Using project name for domain:', domain); + console.log("🌐 Using project name for domain:", domain); } else { const altMatch = inspectOutput.match(/Found Project [^/]+\/([^\n]+)/); if (altMatch) { projectName = altMatch[1].trim(); domain = `${projectName}.vercel.app`; - console.log('🌐 Using project name for domain:', domain); + console.log("🌐 Using project name for domain:", domain); } else { - throw new Error('Could not determine project name from inspection output'); + throw new Error( + "Could not determine project name from inspection output" + ); } } } - // Generate mini app metadata if we have a seed phrase - let miniAppMetadata; - let fid; - if (process.env.SEED_PHRASE) { - console.log('\nšŸ”Ø Generating mini app metadata...'); - const accountAddress = await validateSeedPhrase(process.env.SEED_PHRASE); - fid = await lookupFidByCustodyAddress(accountAddress, process.env.NEYNAR_API_KEY ?? 'FARCASTER_V2_FRAMES_DEMO'); - - const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID + // Generate mini app metadata + console.log("\nšŸ”Ø Generating mini app metadata..."); + + const webhookUrl = + process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `https://${domain}/api/webhook`; - miniAppMetadata = await generateFarcasterMetadata(domain, fid, accountAddress, process.env.SEED_PHRASE, webhookUrl); - console.log('āœ… Mini app metadata generated and signed'); - } + const miniAppMetadata = await generateFarcasterMetadata(domain, webhookUrl); + console.log("āœ… Mini app metadata generated"); // Prepare environment variables - const nextAuthSecret = process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex'); + const nextAuthSecret = + process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex"); const vercelEnv = { NEXTAUTH_SECRET: nextAuthSecret, AUTH_SECRET: nextAuthSecret, NEXTAUTH_URL: `https://${domain}`, NEXT_PUBLIC_URL: `https://${domain}`, - - ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), - ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }), + + ...(process.env.NEYNAR_API_KEY && { + NEYNAR_API_KEY: process.env.NEYNAR_API_KEY, + }), + ...(process.env.NEYNAR_CLIENT_ID && { + NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID, + }), ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }), - + ...Object.fromEntries( - Object.entries(process.env) - .filter(([key]) => key.startsWith('NEXT_PUBLIC_')) - ) + Object.entries(process.env).filter(([key]) => + key.startsWith("NEXT_PUBLIC_") + ) + ), }; // Set environment variables - await setEnvironmentVariables(vercelClient, projectId, vercelEnv, projectRoot); + await setEnvironmentVariables( + vercelClient, + projectId, + vercelEnv, + projectRoot + ); // Deploy the project if (useGitHub) { - console.log('\nSetting up GitHub integration...'); - execSync('vercel link', { + console.log("\nSetting up GitHub integration..."); + execSync("vercel link", { cwd: projectRoot, - stdio: 'inherit', - env: process.env + stdio: "inherit", + env: process.env, }); - console.log('\nšŸ“¦ Deploying with GitHub integration...'); + console.log("\nšŸ“¦ Deploying with GitHub integration..."); } else { - console.log('\nšŸ“¦ Deploying local code directly...'); + console.log("\nšŸ“¦ Deploying local code directly..."); } - execSync('vercel deploy --prod', { + execSync("vercel deploy --prod", { cwd: projectRoot, - stdio: 'inherit', - env: process.env + stdio: "inherit", + env: process.env, }); // Verify actual domain after deployment - console.log('\nšŸ” Verifying deployment domain...'); - + console.log("\nšŸ” Verifying deployment domain..."); + let actualDomain = domain; if (vercelClient) { try { const deployments = await vercelClient.deployments.list({ projectId: projectId, - limit: 1 + limit: 1, }); - + if (deployments.deployments?.[0]?.url) { actualDomain = deployments.deployments[0].url; - console.log('🌐 Verified actual domain:', actualDomain); + console.log("🌐 Verified actual domain:", actualDomain); } } catch (error) { - console.warn('āš ļø Could not verify domain via SDK, using assumed domain'); + console.warn( + "āš ļø Could not verify domain via SDK, using assumed domain" + ); } } // Update environment variables if domain changed if (actualDomain !== domain) { - console.log('šŸ”„ Updating environment variables with correct domain...'); - - const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID - ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` - : `https://${actualDomain}/api/webhook`; + console.log("šŸ”„ Updating environment variables with correct domain..."); + + const webhookUrl = + process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID + ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` + : `https://${actualDomain}/api/webhook`; const updatedEnv = { NEXTAUTH_URL: `https://${actualDomain}`, - NEXT_PUBLIC_URL: `https://${actualDomain}` + NEXT_PUBLIC_URL: `https://${actualDomain}`, }; - if (miniAppMetadata) { - const updatedMetadata = await generateFarcasterMetadata(actualDomain, fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); - updatedEnv.MINI_APP_METADATA = updatedMetadata; - } + const updatedMetadata = await generateFarcasterMetadata( + actualDomain, + webhookUrl + ); + updatedEnv.MINI_APP_METADATA = updatedMetadata; - await setEnvironmentVariables(vercelClient, projectId, updatedEnv, projectRoot); + await setEnvironmentVariables( + vercelClient, + projectId, + updatedEnv, + projectRoot + ); - console.log('\nšŸ“¦ Redeploying with correct domain...'); - execSync('vercel deploy --prod', { + console.log("\nšŸ“¦ Redeploying with correct domain..."); + execSync("vercel deploy --prod", { cwd: projectRoot, - stdio: 'inherit', - env: process.env + stdio: "inherit", + env: process.env, }); - + domain = actualDomain; } - - console.log('\n✨ Deployment complete! Your mini app is now live at:'); - console.log(`🌐 https://${domain}`); - console.log('\nšŸ“ You can manage your project at https://vercel.com/dashboard'); + console.log("\n✨ Deployment complete! Your mini app is now live at:"); + console.log(`🌐 https://${domain}`); + console.log( + "\nšŸ“ You can manage your project at https://vercel.com/dashboard" + ); } catch (error) { - console.error('\nāŒ Deployment failed:', error.message); + console.error("\nāŒ Deployment failed:", error.message); process.exit(1); } } async function main() { try { - console.log('šŸš€ Vercel Mini App Deployment (SDK Edition)'); - console.log('This script will deploy your mini app to Vercel using the Vercel SDK.'); - console.log('\nThe script will:'); - console.log('1. Check for required environment variables'); - console.log('2. Set up a Vercel project (new or existing)'); - console.log('3. Configure environment variables in Vercel using SDK'); - console.log('4. Deploy and build your mini app\n'); + console.log("šŸš€ Vercel Mini App Deployment (SDK Edition)"); + console.log( + "This script will deploy your mini app to Vercel using the Vercel SDK." + ); + console.log("\nThe script will:"); + console.log("1. Check for required environment variables"); + console.log("2. Set up a Vercel project (new or existing)"); + console.log("3. Configure environment variables in Vercel using SDK"); + console.log("4. Deploy and build your mini app\n"); // Check if @vercel/sdk is installed try { - await import('@vercel/sdk'); + await import("@vercel/sdk"); } catch (error) { - console.log('šŸ“¦ Installing @vercel/sdk...'); - execSync('npm install @vercel/sdk', { + console.log("šŸ“¦ Installing @vercel/sdk..."); + execSync("npm install @vercel/sdk", { cwd: projectRoot, - stdio: 'inherit' + stdio: "inherit", }); - console.log('āœ… @vercel/sdk installed successfully'); + console.log("āœ… @vercel/sdk installed successfully"); } await checkRequiredEnvVars(); @@ -703,58 +678,57 @@ async function main() { let useGitHub = false; if (remoteUrl) { - console.log('\nšŸ“¦ Found GitHub repository:', remoteUrl); + console.log("\nšŸ“¦ Found GitHub repository:", remoteUrl); const { useGitHubDeploy } = await inquirer.prompt([ { - type: 'confirm', - name: 'useGitHubDeploy', - message: 'Would you like to deploy from the GitHub repository?', - default: true - } + type: "confirm", + name: "useGitHubDeploy", + message: "Would you like to deploy from the GitHub repository?", + default: true, + }, ]); useGitHub = useGitHubDeploy; } else { - console.log('\nāš ļø No GitHub repository found.'); + console.log("\nāš ļø No GitHub repository found."); const { action } = await inquirer.prompt([ { - type: 'list', - name: 'action', - message: 'What would you like to do?', + type: "list", + name: "action", + message: "What would you like to do?", choices: [ - { name: 'Deploy local code directly', value: 'deploy' }, - { name: 'Set up GitHub repository first', value: 'setup' } + { name: "Deploy local code directly", value: "deploy" }, + { name: "Set up GitHub repository first", value: "setup" }, ], - default: 'deploy' - } + default: "deploy", + }, ]); - if (action === 'setup') { - console.log('\nšŸ‘‹ Please set up your GitHub repository first:'); - console.log('1. Create a new repository on GitHub'); - console.log('2. Run these commands:'); - console.log(' git remote add origin '); - console.log(' git push -u origin main'); - console.log('\nThen run this script again to deploy.'); + if (action === "setup") { + console.log("\nšŸ‘‹ Please set up your GitHub repository first:"); + console.log("1. Create a new repository on GitHub"); + console.log("2. Run these commands:"); + console.log(" git remote add origin "); + console.log(" git push -u origin main"); + console.log("\nThen run this script again to deploy."); process.exit(0); } } - if (!await checkVercelCLI()) { - console.log('Vercel CLI not found. Installing...'); + if (!(await checkVercelCLI())) { + console.log("Vercel CLI not found. Installing..."); await installVercelCLI(); } - if (!await loginToVercel()) { - console.error('\nāŒ Failed to log in to Vercel. Please try again.'); + if (!(await loginToVercel())) { + console.error("\nāŒ Failed to log in to Vercel. Please try again."); process.exit(1); } await deployToVercel(useGitHub); - } catch (error) { - console.error('\nāŒ Error:', error.message); + console.error("\nāŒ Error:", error.message); process.exit(1); } } -main(); \ No newline at end of file +main(); diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 85168a0..0762d67 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,8 +1,19 @@ -import { type ClassValue, clsx } from 'clsx'; -import { twMerge } from 'tailwind-merge'; -import { mnemonicToAccount } from 'viem/accounts'; -import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants'; -import { APP_SPLASH_URL } from './constants'; +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; +import { mnemonicToAccount } from "viem/accounts"; +import { + APP_BUTTON_TEXT, + APP_DESCRIPTION, + APP_ICON_URL, + APP_NAME, + APP_OG_IMAGE_URL, + APP_PRIMARY_CATEGORY, + APP_SPLASH_BACKGROUND_COLOR, + APP_TAGS, + APP_URL, + APP_WEBHOOK_URL, +} from "./constants"; +import { APP_SPLASH_URL } from "./constants"; interface MiniAppMetadata { version: string; @@ -17,7 +28,7 @@ interface MiniAppMetadata { description?: string; primaryCategory?: string; tags?: string[]; -}; +} interface MiniAppManifest { accountAssociation?: { @@ -32,17 +43,6 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } -export function getSecretEnvVars() { - const seedPhrase = process.env.SEED_PHRASE; - const fid = process.env.FID; - - if (!seedPhrase || !fid) { - return null; - } - - return { seedPhrase, fid }; -} - export function getMiniAppEmbedMetadata(ogImageUrl?: string) { return { version: "next", @@ -69,58 +69,30 @@ export async function getFarcasterMetadata(): Promise { if (process.env.MINI_APP_METADATA) { try { const metadata = JSON.parse(process.env.MINI_APP_METADATA); - console.log('Using pre-signed mini app metadata from environment'); + console.log("Using pre-signed mini app metadata from environment"); return metadata; } catch (error) { - console.warn('Failed to parse MINI_APP_METADATA from environment:', error); + console.warn( + "Failed to parse MINI_APP_METADATA from environment:", + error + ); } } if (!APP_URL) { - throw new Error('NEXT_PUBLIC_URL not configured'); + throw new Error("NEXT_PUBLIC_URL not configured"); } // Get the domain from the URL (without https:// prefix) const domain = new URL(APP_URL).hostname; - console.log('Using domain for manifest:', domain); - - const secretEnvVars = getSecretEnvVars(); - if (!secretEnvVars) { - console.warn('No seed phrase or FID found in environment variables -- generating unsigned metadata'); - } - - let accountAssociation; - if (secretEnvVars) { - // Generate account from seed phrase - const account = mnemonicToAccount(secretEnvVars.seedPhrase); - const custodyAddress = account.address; - - const header = { - fid: parseInt(secretEnvVars.fid), - type: 'custody', - key: custodyAddress, - }; - const encodedHeader = Buffer.from(JSON.stringify(header), 'utf-8').toString('base64'); - - const payload = { - domain - }; - const encodedPayload = Buffer.from(JSON.stringify(payload), 'utf-8').toString('base64url'); - - const signature = await account.signMessage({ - message: `${encodedHeader}.${encodedPayload}` - }); - const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); - - accountAssociation = { - header: encodedHeader, - payload: encodedPayload, - signature: encodedSignature - }; - } + console.log("Using domain for manifest:", domain); return { - accountAssociation, + accountAssociation: { + header: "", + payload: "", + signature: "", + }, frame: { version: "1", name: APP_NAME ?? "Neynar Starter Kit", From 822147d0c7b2cd2c6d7c2b8830f342e99b15b5fe Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 16:32:16 +0530 Subject: [PATCH 09/32] remove siwe from package.json --- package-lock.json | 176 ---------------------------------------------- package.json | 1 - 2 files changed, 177 deletions(-) diff --git a/package-lock.json b/package-lock.json index 21e3f9b..6dc240c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,6 @@ "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", - "siwe": "^3.0.0", "viem": "^2.23.6" }, "bin": { @@ -625,47 +624,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@spruceid/siwe-parser": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@spruceid/siwe-parser/-/siwe-parser-3.0.0.tgz", - "integrity": "sha512-Y92k63ilw/8jH9Ry4G2e7lQd0jZAvb0d/Q7ssSD0D9mp/Zt2aCXIc3g0ny9yhplpAx1QXHsMz/JJptHK/zDGdw==", - "license": "Apache-2.0", - "dependencies": { - "@noble/hashes": "^1.1.2", - "apg-js": "^4.4.0" - } - }, - "node_modules/@stablelib/binary": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz", - "integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==", - "license": "MIT", - "dependencies": { - "@stablelib/int": "^1.0.1" - } - }, - "node_modules/@stablelib/int": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz", - "integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==", - "license": "MIT" - }, - "node_modules/@stablelib/random": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@stablelib/random/-/random-1.0.2.tgz", - "integrity": "sha512-rIsE83Xpb7clHPVRlBj8qNe5L8ISQOzjghYQm/dZ7VaM2KHYwMW5adjQjrzTZCchFnNCNhkwtnOBa9HTMJCI8w==", - "license": "MIT", - "dependencies": { - "@stablelib/binary": "^1.0.1", - "@stablelib/wipe": "^1.0.1" - } - }, - "node_modules/@stablelib/wipe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz", - "integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==", - "license": "MIT" - }, "node_modules/@tootallnate/quickjs-emscripten": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", @@ -704,13 +662,6 @@ } } }, - "node_modules/aes-js": { - "version": "4.0.0-beta.5", - "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", - "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==", - "license": "MIT", - "peer": true - }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -760,12 +711,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/apg-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/apg-js/-/apg-js-4.4.0.tgz", - "integrity": "sha512-fefmXFknJmtgtNEXfPwZKYkMFX4Fyeyz+fNF6JWp87biGOPslJbCBVU158zvKRZfHBKnJDy8CMM40oLFGkXT8Q==", - "license": "BSD-2-Clause" - }, "node_modules/ast-types": { "version": "0.13.4", "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", @@ -1373,114 +1318,6 @@ "node": ">=0.10.0" } }, - "node_modules/ethers": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz", - "integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/ethers-io/" - }, - { - "type": "individual", - "url": "https://www.buymeacoffee.com/ricmoo" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "@adraffy/ens-normalize": "1.10.1", - "@noble/curves": "1.2.0", - "@noble/hashes": "1.3.2", - "@types/node": "22.7.5", - "aes-js": "4.0.0-beta.5", - "tslib": "2.7.0", - "ws": "8.17.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/ethers/node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", - "license": "MIT", - "peer": true - }, - "node_modules/ethers/node_modules/@noble/curves": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", - "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", - "license": "MIT", - "peer": true, - "dependencies": { - "@noble/hashes": "1.3.2" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethers/node_modules/@noble/hashes": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", - "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethers/node_modules/@types/node": { - "version": "22.7.5", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz", - "integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==", - "license": "MIT", - "peer": true, - "dependencies": { - "undici-types": "~6.19.2" - } - }, - "node_modules/ethers/node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", - "license": "0BSD", - "peer": true - }, - "node_modules/ethers/node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", - "license": "MIT", - "peer": true - }, - "node_modules/ethers/node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -2393,19 +2230,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/siwe": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/siwe/-/siwe-3.0.0.tgz", - "integrity": "sha512-P2/ry7dHYJA6JJ5+veS//Gn2XDwNb3JMvuD6xiXX8L/PJ1SNVD4a3a8xqEbmANx+7kNQcD8YAh1B9bNKKvRy/g==", - "license": "Apache-2.0", - "dependencies": { - "@spruceid/siwe-parser": "^3.0.0", - "@stablelib/random": "^1.0.1" - }, - "peerDependencies": { - "ethers": "^5.6.8 || ^6.0.8" - } - }, "node_modules/smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index 0709c25..3666086 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", - "siwe": "^3.0.0", "viem": "^2.23.6" }, "devDependencies": { From ea52f72a7315cdc4aeee51569b18ed62fb99ef2b Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 16:33:27 +0530 Subject: [PATCH 10/32] Bumpup patch version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3666086..a42cc59 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.3", + "version": "1.5.4", "type": "module", "private": false, "access": "public", From cc84b0a88230df4acca697aeb7bc9dd859acdea3 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 16:36:55 +0530 Subject: [PATCH 11/32] formatting --- src/app/providers.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 90584eb..2b335d8 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -32,7 +32,9 @@ export function Providers({ backButtonEnabled={true} > - {children} + + {children} + From bbc8d81613add72a8e4143d3b95d5d243df931cb Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 20:19:22 +0530 Subject: [PATCH 12/32] more refactor --- src/components/ui/NeynarAuthButton.tsx | 891 ------------------ .../ui/NeynarAuthButton/AuthDialog.tsx | 222 +++++ .../ui/NeynarAuthButton/ProfileButton.tsx | 92 ++ src/components/ui/NeynarAuthButton/index.tsx | 511 ++++++++++ src/components/ui/tabs/ActionsTab.tsx | 2 +- src/hooks/useDetectClickOutside.ts | 18 + src/lib/devices.ts | 27 + src/lib/localStorage.ts | 25 + 8 files changed, 896 insertions(+), 892 deletions(-) delete mode 100644 src/components/ui/NeynarAuthButton.tsx create mode 100644 src/components/ui/NeynarAuthButton/AuthDialog.tsx create mode 100644 src/components/ui/NeynarAuthButton/ProfileButton.tsx create mode 100644 src/components/ui/NeynarAuthButton/index.tsx create mode 100644 src/hooks/useDetectClickOutside.ts create mode 100644 src/lib/devices.ts create mode 100644 src/lib/localStorage.ts diff --git a/src/components/ui/NeynarAuthButton.tsx b/src/components/ui/NeynarAuthButton.tsx deleted file mode 100644 index 417c6c1..0000000 --- a/src/components/ui/NeynarAuthButton.tsx +++ /dev/null @@ -1,891 +0,0 @@ -'use client'; - -import '@farcaster/auth-kit/styles.css'; -import { useSignIn } from '@farcaster/auth-kit'; -import { useCallback, useEffect, useState, useRef } from 'react'; -import { cn } from '../../lib/utils'; -import { Button } from './Button'; - -// Utility functions for device detection -function isAndroid(): boolean { - return ( - typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent) - ); -} - -function isSmallIOS(): boolean { - return ( - typeof navigator !== 'undefined' && /iPhone|iPod/.test(navigator.userAgent) - ); -} - -function isLargeIOS(): boolean { - return ( - typeof navigator !== 'undefined' && - (/iPad/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) - ); -} - -function isIOS(): boolean { - return isSmallIOS() || isLargeIOS(); -} - -function isMobile(): boolean { - return isAndroid() || isIOS(); -} - -// Hook for detecting clicks outside an element -function useDetectClickOutside( - ref: React.RefObject, - callback: () => void -) { - useEffect(() => { - function handleClickOutside(event: MouseEvent) { - if (ref.current && !ref.current.contains(event.target as Node)) { - callback(); - } - } - document.addEventListener('mousedown', handleClickOutside); - return () => { - document.removeEventListener('mousedown', handleClickOutside); - }; - }, [ref, callback]); -} - -// Storage utilities for persistence -const STORAGE_KEY = 'farcaster_auth_state'; - -interface StoredAuthState { - isAuthenticated: boolean; - userData?: { - fid?: number; - pfpUrl?: string; - username?: string; - }; - lastSignInTime?: number; - signers?: { - object: 'signer'; - signer_uuid: string; - public_key: string; - status: 'approved'; - fid: number; - }[]; // Store the list of signers -} - -function saveAuthState(state: StoredAuthState) { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); - } catch (error) { - console.warn('Failed to save auth state:', error); - } -} - -function loadAuthState(): StoredAuthState | null { - try { - const stored = localStorage.getItem(STORAGE_KEY); - return stored ? JSON.parse(stored) : null; - } catch (error) { - console.warn('Failed to load auth state:', error); - return null; - } -} - -function clearAuthState() { - try { - localStorage.removeItem(STORAGE_KEY); - } catch (error) { - console.warn('Failed to clear auth state:', error); - } -} - -function updateSignersInAuthState( - signers: StoredAuthState['signers'] -): StoredAuthState | null { - try { - const stored = loadAuthState(); - if (stored) { - const updatedState = { ...stored, signers }; - saveAuthState(updatedState); - return updatedState; - } - } catch (error) { - console.warn('Failed to update signers in auth state:', error); - } - return null; -} - -export function getStoredSigners(): unknown[] { - try { - const stored = loadAuthState(); - return stored?.signers || []; - } catch (error) { - console.warn('Failed to get stored signers:', error); - return []; - } -} - -// Enhanced QR Code Dialog Component with multiple steps -function AuthDialog({ - open, - onClose, - url, - isError, - error, - step, - isLoading, - signerApprovalUrl, -}: { - open: boolean; - onClose: () => void; - url: string; - isError: boolean; - error?: Error | null; - step: 'signin' | 'access' | 'loading'; - isLoading?: boolean; - signerApprovalUrl?: string | null; -}) { - if (!open) return null; - - const getStepContent = () => { - switch (step) { - case 'signin': - return { - title: 'Signin', - description: - "To signin, scan the code below with your phone's camera.", - showQR: true, - qrUrl: url, - showOpenButton: true, - }; - - case 'loading': - return { - title: 'Setting up access...', - description: - 'Checking your account permissions and setting up secure access.', - showQR: false, - qrUrl: '', - showOpenButton: false, - }; - - case 'access': - return { - title: 'Grant Access', - description: ( -
-

- Allow this app to access your Farcaster account: -

-
-
-
- - - -
-
-
- Read Access -
-
- View your profile and public information -
-
-
-
-
- - - -
-
-
- Write Access -
-
- Post casts, likes, and update your profile -
-
-
-
-
- ), - // Show QR code if we have signer approval URL, otherwise show loading - showQR: !!signerApprovalUrl, - qrUrl: signerApprovalUrl || '', - showOpenButton: !!signerApprovalUrl, - }; - - default: - return { - title: 'Sign in', - description: - "To signin, scan the code below with your phone's camera.", - showQR: true, - qrUrl: url, - showOpenButton: true, - }; - } - }; - - const content = getStepContent(); - - return ( -
-
-
-

- {isError ? 'Error' : content.title} -

- -
- - {isError ? ( -
-
- {error?.message || 'Unknown error, please try again.'} -
- -
- ) : ( -
-
- {typeof content.description === 'string' ? ( -

- {content.description} -

- ) : ( - content.description - )} -
- -
- {content.showQR && content.qrUrl ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - QR Code -
- ) : step === 'loading' || isLoading ? ( -
-
-
- - {step === 'loading' - ? 'Setting up access...' - : 'Loading...'} - -
-
- ) : null} -
- - {content.showOpenButton && content.qrUrl && ( - - )} -
- )} -
-
- ); -} - -// Profile Button Component -function ProfileButton({ - userData, - onSignOut, -}: { - userData?: { fid?: number; pfpUrl?: string; username?: string }; - onSignOut: () => void; -}) { - const [showDropdown, setShowDropdown] = useState(false); - const ref = useRef(null); - - useDetectClickOutside(ref, () => setShowDropdown(false)); - - const name = userData?.username ?? `!${userData?.fid}`; - const pfpUrl = userData?.pfpUrl ?? 'https://farcaster.xyz/avatar.png'; - - return ( -
- - - {showDropdown && ( -
- -
- )} -
- ); -} - -// Main Custom SignInButton Component -export function NeynarAuthButton() { - const [nonce, setNonce] = useState(null); - const [storedAuth, setStoredAuth] = useState(null); - const [signersLoading, setSignersLoading] = useState(false); - - // New state for unified dialog flow - const [showDialog, setShowDialog] = useState(false); - const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( - 'loading' - ); - const [signerApprovalUrl, setSignerApprovalUrl] = useState( - null - ); - const [pollingInterval, setPollingInterval] = useState( - null - ); - - // Helper function to create a signer - const createSigner = useCallback(async () => { - try { - // console.log('šŸ”§ Creating new signer...'); - - const response = await fetch('/api/auth/signer', { - method: 'POST', - }); - - if (!response.ok) { - throw new Error('Failed to create signer'); - } - - const signerData = await response.json(); - // console.log('āœ… Signer created:', signerData); - - return signerData; - } catch (error) { - // console.error('āŒ Error creating signer:', error); - throw error; - } - }, []); - - // Helper function to generate signed key request - const generateSignedKeyRequest = useCallback( - async (signerUuid: string, publicKey: string) => { - try { - // console.log('šŸ”‘ Generating signed key request...'); - - // Prepare request body - const requestBody: { - signerUuid: string; - publicKey: string; - sponsor?: { sponsored_by_neynar: boolean }; - } = { - signerUuid, - publicKey, - }; - - const response = await fetch('/api/auth/signer/signed_key', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify(requestBody), - }); - - if (!response.ok) { - const errorData = await response.json(); - throw new Error( - `Failed to generate signed key request: ${errorData.error}` - ); - } - - const data = await response.json(); - // console.log('āœ… Signed key request generated:', data); - - return data; - } catch (error) { - console.error('āŒ Error generating signed key request:', error); - throw error; - } - }, - [] - ); - - // Helper function to fetch all signers - const fetchAllSigners = useCallback( - async (message: string, signature: string) => { - try { - // console.log('ļæ½ Fetching all signers...'); - setSignersLoading(true); - - const response = await fetch( - `/api/auth/signers?message=${encodeURIComponent( - message - )}&signature=${signature}` - ); - - const signerData = await response.json(); - // console.log('ļæ½ Signer response:', signerData); - - if (response.ok) { - // console.log('āœ… Signers fetched successfully:', signerData.signers); - - // Store signers in localStorage - const updatedState = updateSignersInAuthState( - signerData.signers || [] - ); - if (updatedState) { - setStoredAuth(updatedState); - } - - return signerData.signers; - } else { - console.error('āŒ Failed to fetch signers'); - throw new Error('Failed to fetch signers'); - } - } catch (error) { - console.error('āŒ Error fetching signers:', error); - throw error; - } finally { - setSignersLoading(false); - } - }, - [] - ); - - // Helper function to poll signer status - const startPolling = useCallback( - (signerUuid: string, message: string, signature: string) => { - // console.log('ļæ½ Starting polling for signer:', signerUuid); - - const interval = setInterval(async () => { - try { - const response = await fetch( - `/api/auth/signer?signerUuid=${signerUuid}` - ); - - if (!response.ok) { - throw new Error('Failed to poll signer status'); - } - - const signerData = await response.json(); - // console.log('ļæ½ Signer status:', signerData.status); - - if (signerData.status === 'approved') { - // console.log('šŸŽ‰ Signer approved!'); - clearInterval(interval); - setPollingInterval(null); - setShowDialog(false); - setDialogStep('signin'); - setSignerApprovalUrl(null); - - // Refetch all signers - await fetchAllSigners(message, signature); - } - } catch (error) { - console.error('āŒ Error polling signer:', error); - } - }, 1000); // Poll every 1 second - - setPollingInterval(interval); - }, - [fetchAllSigners] - ); - - // Cleanup polling on unmount - useEffect(() => { - return () => { - if (pollingInterval) { - clearInterval(pollingInterval); - } - }; - }, [pollingInterval]); - - // Generate nonce - useEffect(() => { - const generateNonce = async () => { - try { - const response = await fetch('/api/auth/nonce'); - if (response.ok) { - const data = await response.json(); - setNonce(data.nonce); - } else { - console.error('Failed to fetch nonce'); - } - } catch (error) { - console.error('Error generating nonce:', error); - } - }; - - generateNonce(); - }, []); - - // Load stored auth state on mount - useEffect(() => { - const stored = loadAuthState(); - if (stored && stored.isAuthenticated) { - setStoredAuth(stored); - if (stored.signers && stored.signers.length > 0) { - // console.log('šŸ“‚ Loaded stored signers:', stored.signers); - } - } - }, []); - - // Success callback - this is critical! - const onSuccessCallback = useCallback((res: unknown) => { - // console.log('šŸŽ‰ Sign in successful!', res); - const authState: StoredAuthState = { - isAuthenticated: true, - userData: res as StoredAuthState['userData'], - lastSignInTime: Date.now(), - }; - saveAuthState(authState); - setStoredAuth(authState); - // setShowDialog(false); - }, []); - - // 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, - signOut, - connect, - reconnect, - isSuccess, - isError, - error, - channelToken, - url, - data, - validSignature, - isPolling, - } = signInState; - - // Connect when component mounts and we have a nonce - useEffect(() => { - if (nonce && !channelToken) { - // console.log('šŸ”Œ Connecting with nonce:', nonce); - connect(); - } - }, [nonce, channelToken, connect]); - - // Debug logging - // useEffect(() => { - // console.log('šŸ” Auth state:', { - // isSuccess, - // validSignature, - // hasData: !!data, - // isPolling, - // isError, - // storedAuth: !!storedAuth?.isAuthenticated, - // }); - // }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); - - // Handle fetching signers after successful authentication - useEffect(() => { - if (data?.message && data?.signature) { - // console.log('šŸ“ Got message and signature:', { - // message: data.message, - // signature: data.signature, - // }); - const handleSignerFlow = async () => { - try { - // Ensure we have message and signature - if (!data.message || !data.signature) { - console.error('āŒ Missing message or signature'); - return; - } - - // Step 1: Change to loading state - setDialogStep('loading'); - setSignersLoading(true); - - // First, fetch existing signers - const signers = await fetchAllSigners(data.message, data.signature); - - // Check if no signers exist - if (!signers || signers.length === 0) { - // console.log('ļæ½ No signers found, creating new signer...'); - - // Step 1: Create a signer - const newSigner = await createSigner(); - - // Step 2: Generate signed key request - const signedKeyData = await generateSignedKeyRequest( - newSigner.signer_uuid, - newSigner.public_key - ); - - // Step 3: Show QR code in access dialog for signer approval - if (signedKeyData.signer_approval_url) { - setSignerApprovalUrl(signedKeyData.signer_approval_url); - setSignersLoading(false); // Stop loading, show QR code - setDialogStep('access'); // Switch to access step to show QR - - // Step 4: Start polling for signer approval - startPolling(newSigner.signer_uuid, data.message, data.signature); - } - } else { - // If signers exist, close the dialog - // console.log('āœ… Signers already exist, closing dialog'); - setSignersLoading(false); - setShowDialog(false); - setDialogStep('signin'); - } - } catch (error) { - console.error('āŒ Error in signer flow:', error); - // On error, reset to signin step - setDialogStep('signin'); - setSignersLoading(false); - } - }; - - handleSignerFlow(); - } - }, [ - data?.message, - data?.signature, - fetchAllSigners, - createSigner, - generateSignedKeyRequest, - startPolling, - ]); - - const handleSignIn = useCallback(() => { - // console.log('šŸš€ Starting sign in flow...'); - if (isError) { - // console.log('šŸ”„ Reconnecting due to error...'); - reconnect(); - } - setDialogStep('signin'); - setShowDialog(true); - signIn(); - - // Open mobile app if on mobile and URL is available - if (url && isMobile()) { - // console.log('šŸ“± Opening mobile app:', url); - window.open(url, '_blank'); - } - }, [isError, reconnect, signIn, url]); - - const handleSignOut = useCallback(() => { - // console.log('šŸ‘‹ Signing out...'); - setShowDialog(false); - signOut(); - clearAuthState(); - setStoredAuth(null); - }, [signOut]); - - // The key fix: match the original library's authentication logic exactly - const authenticated = - ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && - !!(storedAuth?.signers && storedAuth.signers.length > 0); - const userData = data || storedAuth?.userData; - - // Show loading state while nonce is being fetched or signers are loading - if (!nonce || signersLoading) { - return ( -
-
-
- - Loading... - -
-
- ); - } - - return ( - <> - {authenticated ? ( - - ) : ( - - )} - - {/* Unified Auth Dialog */} - {url && ( - { - setShowDialog(false); - setDialogStep('signin'); - setSignerApprovalUrl(null); - if (pollingInterval) { - clearInterval(pollingInterval); - setPollingInterval(null); - } - }} - url={url} - isError={isError} - error={error} - step={dialogStep} - isLoading={signersLoading} - signerApprovalUrl={signerApprovalUrl} - /> - )} - - {/* Debug panel (optional - can be removed in production) */} - {/* {process.env.NODE_ENV === "development" && ( -
-
Debug Info:
-
-            {JSON.stringify(
-              {
-                authenticated,
-                isSuccess,
-                validSignature,
-                hasData: !!data,
-                isPolling,
-                isError,
-                hasStoredAuth: !!storedAuth?.isAuthenticated,
-                hasUrl: !!url,
-                hasChannelToken: !!channelToken,
-              },
-              null,
-              2
-            )}
-          
-
- )} */} - - ); -} diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx new file mode 100644 index 0000000..8c8f984 --- /dev/null +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -0,0 +1,222 @@ +'use client'; + +export function AuthDialog({ + open, + onClose, + url, + isError, + error, + step, + isLoading, + signerApprovalUrl, +}: { + open: boolean; + onClose: () => void; + url: string; + isError: boolean; + error?: Error | null; + step: 'signin' | 'access' | 'loading'; + isLoading?: boolean; + signerApprovalUrl?: string | null; +}) { + if (!open) return null; + + const getStepContent = () => { + switch (step) { + case 'signin': + return { + title: 'Signin', + description: + "To signin, scan the code below with your phone's camera.", + showQR: true, + qrUrl: url, + showOpenButton: true, + }; + + case 'loading': + return { + title: 'Setting up access...', + description: + 'Checking your account permissions and setting up secure access.', + showQR: false, + qrUrl: '', + showOpenButton: false, + }; + + case 'access': + return { + title: 'Grant Access', + description: ( +
+

+ Allow this app to access your Farcaster account: +

+
+
+
+ + + +
+
+
+ Read Access +
+
+ View your profile and public information +
+
+
+
+
+ + + +
+
+
+ Write Access +
+
+ Post casts, likes, and update your profile +
+
+
+
+
+ ), + // Show QR code if we have signer approval URL, otherwise show loading + showQR: !!signerApprovalUrl, + qrUrl: signerApprovalUrl || '', + showOpenButton: !!signerApprovalUrl, + }; + + default: + return { + title: 'Sign in', + description: + "To signin, scan the code below with your phone's camera.", + showQR: true, + qrUrl: url, + showOpenButton: true, + }; + } + }; + + const content = getStepContent(); + + return ( +
+
+
+

+ {isError ? 'Error' : content.title} +

+ +
+ + {isError ? ( +
+
+ {error?.message || 'Unknown error, please try again.'} +
+ +
+ ) : ( +
+
+ {typeof content.description === 'string' ? ( +

+ {content.description} +

+ ) : ( + content.description + )} +
+ +
+ {content.showQR && content.qrUrl ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + QR Code +
+ ) : step === 'loading' || isLoading ? ( +
+
+
+ + {step === 'loading' + ? 'Setting up access...' + : 'Loading...'} + +
+
+ ) : null} +
+ + {content.showOpenButton && content.qrUrl && ( + + )} +
+ )} +
+
+ ); +} diff --git a/src/components/ui/NeynarAuthButton/ProfileButton.tsx b/src/components/ui/NeynarAuthButton/ProfileButton.tsx new file mode 100644 index 0000000..daec476 --- /dev/null +++ b/src/components/ui/NeynarAuthButton/ProfileButton.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { useRef, useState } from 'react'; +import { useDetectClickOutside } from '~/hooks/useDetectClickOutside'; +import { cn } from '~/lib/utils'; + +export function ProfileButton({ + userData, + onSignOut, +}: { + userData?: { fid?: number; pfpUrl?: string; username?: string }; + onSignOut: () => void; +}) { + const [showDropdown, setShowDropdown] = useState(false); + const ref = useRef(null); + + useDetectClickOutside(ref, () => setShowDropdown(false)); + + const name = userData?.username ?? `!${userData?.fid}`; + const pfpUrl = userData?.pfpUrl ?? 'https://farcaster.xyz/avatar.png'; + + return ( +
+ + + {showDropdown && ( +
+ +
+ )} +
+ ); +} diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx new file mode 100644 index 0000000..0a2fea0 --- /dev/null +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -0,0 +1,511 @@ +'use client'; + +import '@farcaster/auth-kit/styles.css'; +import { useSignIn } from '@farcaster/auth-kit'; +import { useCallback, useEffect, useState } from 'react'; +import { cn } from '~/lib/utils'; +import { Button } from '~/components/ui/Button'; +import { isMobile } from '~/lib/devices'; +import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton'; +import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog'; +import { getItem, removeItem, setItem } from '~/lib/localStorage'; + +const STORAGE_KEY = 'neynar_authenticated_user'; + +interface StoredAuthState { + isAuthenticated: boolean; + userData?: { + fid?: number; + pfpUrl?: string; + username?: string; + }; + lastSignInTime?: number; + signers?: { + object: 'signer'; + signer_uuid: string; + public_key: string; + status: 'approved'; + fid: number; + }[]; // Store the list of signers +} + +// Main Custom SignInButton Component +export function NeynarAuthButton() { + const [nonce, setNonce] = useState(null); + const [storedAuth, setStoredAuth] = useState(null); + const [signersLoading, setSignersLoading] = useState(false); + + // New state for unified dialog flow + const [showDialog, setShowDialog] = useState(false); + const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( + 'loading' + ); + const [signerApprovalUrl, setSignerApprovalUrl] = useState( + null + ); + const [pollingInterval, setPollingInterval] = useState( + null + ); + + // Helper function to create a signer + const createSigner = useCallback(async () => { + try { + // console.log('šŸ”§ Creating new signer...'); + + const response = await fetch('/api/auth/signer', { + method: 'POST', + }); + + if (!response.ok) { + throw new Error('Failed to create signer'); + } + + const signerData = await response.json(); + // console.log('āœ… Signer created:', signerData); + + return signerData; + } catch (error) { + // console.error('āŒ Error creating signer:', error); + throw error; + } + }, []); + + // Helper function to generate signed key request + const generateSignedKeyRequest = useCallback( + async (signerUuid: string, publicKey: string) => { + try { + // console.log('šŸ”‘ Generating signed key request...'); + + // Prepare request body + const requestBody: { + signerUuid: string; + publicKey: string; + sponsor?: { sponsored_by_neynar: boolean }; + } = { + signerUuid, + publicKey, + }; + + const response = await fetch('/api/auth/signer/signed_key', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error( + `Failed to generate signed key request: ${errorData.error}` + ); + } + + const data = await response.json(); + // console.log('āœ… Signed key request generated:', data); + + return data; + } catch (error) { + console.error('āŒ Error generating signed key request:', error); + throw error; + } + }, + [] + ); + + // Helper function to fetch all signers + const fetchAllSigners = useCallback( + async (message: string, signature: string) => { + try { + // console.log('ļæ½ Fetching all signers...'); + setSignersLoading(true); + + const response = await fetch( + `/api/auth/signers?message=${encodeURIComponent( + message + )}&signature=${signature}` + ); + + const signerData = await response.json(); + // console.log('ļæ½ Signer response:', signerData); + + if (response.ok) { + // console.log('āœ… Signers fetched successfully:', signerData.signers); + + // Store signers in localStorage, preserving existing auth data + const existingAuth = getItem(STORAGE_KEY); + const updatedState: StoredAuthState = { + ...existingAuth, + isAuthenticated: true, + signers: signerData.signers || [], + lastSignInTime: existingAuth?.lastSignInTime || Date.now(), + }; + setItem(STORAGE_KEY, updatedState); + setStoredAuth(updatedState); + + return signerData.signers; + } else { + console.error('āŒ Failed to fetch signers'); + throw new Error('Failed to fetch signers'); + } + } catch (error) { + console.error('āŒ Error fetching signers:', error); + throw error; + } finally { + setSignersLoading(false); + } + }, + [] + ); + + // Helper function to poll signer status + const startPolling = useCallback( + (signerUuid: string, message: string, signature: string) => { + // console.log('ļæ½ Starting polling for signer:', signerUuid); + + const interval = setInterval(async () => { + try { + const response = await fetch( + `/api/auth/signer?signerUuid=${signerUuid}` + ); + + if (!response.ok) { + throw new Error('Failed to poll signer status'); + } + + const signerData = await response.json(); + // console.log('ļæ½ Signer status:', signerData.status); + + if (signerData.status === 'approved') { + // console.log('šŸŽ‰ Signer approved!'); + clearInterval(interval); + setPollingInterval(null); + setShowDialog(false); + setDialogStep('signin'); + setSignerApprovalUrl(null); + + // Refetch all signers + await fetchAllSigners(message, signature); + } + } catch (error) { + console.error('āŒ Error polling signer:', error); + } + }, 1000); // Poll every 1 second + + setPollingInterval(interval); + }, + [fetchAllSigners] + ); + + // Cleanup polling on unmount + useEffect(() => { + return () => { + if (pollingInterval) { + clearInterval(pollingInterval); + } + }; + }, [pollingInterval]); + + // Generate nonce + useEffect(() => { + const generateNonce = async () => { + try { + const response = await fetch('/api/auth/nonce'); + if (response.ok) { + const data = await response.json(); + setNonce(data.nonce); + } else { + console.error('Failed to fetch nonce'); + } + } catch (error) { + console.error('Error generating nonce:', error); + } + }; + + generateNonce(); + }, []); + + // Load stored auth state on mount + useEffect(() => { + const stored = getItem(STORAGE_KEY); + if (stored && stored.isAuthenticated) { + setStoredAuth(stored); + if (stored.signers && stored.signers.length > 0) { + // console.log('šŸ“‚ Loaded stored signers:', stored.signers); + } + } + }, []); + + // Success callback - this is critical! + const onSuccessCallback = useCallback((res: unknown) => { + // console.log('šŸŽ‰ Sign in successful!', res); + const existingAuth = getItem(STORAGE_KEY); + const authState: StoredAuthState = { + isAuthenticated: true, + userData: res as StoredAuthState['userData'], + lastSignInTime: Date.now(), + signers: existingAuth?.signers || [], // Preserve existing signers + }; + setItem(STORAGE_KEY, authState); + setStoredAuth(authState); + // setShowDialog(false); + }, []); + + // 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, + signOut, + connect, + reconnect, + isSuccess, + isError, + error, + channelToken, + url, + data, + validSignature, + } = signInState; + + // Connect when component mounts and we have a nonce + useEffect(() => { + if (nonce && !channelToken) { + // console.log('šŸ”Œ Connecting with nonce:', nonce); + connect(); + } + }, [nonce, channelToken, connect]); + + // Debug logging + // useEffect(() => { + // console.log('šŸ” Auth state:', { + // isSuccess, + // validSignature, + // hasData: !!data, + // isPolling, + // isError, + // storedAuth: !!storedAuth?.isAuthenticated, + // }); + // }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); + + // Handle fetching signers after successful authentication + useEffect(() => { + if (data?.message && data?.signature) { + // console.log('šŸ“ Got message and signature:', { + // message: data.message, + // signature: data.signature, + // }); + const handleSignerFlow = async () => { + try { + // Ensure we have message and signature + if (!data.message || !data.signature) { + console.error('āŒ Missing message or signature'); + return; + } + + // Step 1: Change to loading state + setDialogStep('loading'); + setSignersLoading(true); + + // First, fetch existing signers + const signers = await fetchAllSigners(data.message, data.signature); + + // Check if no signers exist or if we have empty signers + if (!signers || signers.length === 0) { + // console.log('šŸ”§ No signers found, creating new signer...'); + + // Step 1: Create a signer + const newSigner = await createSigner(); + + // Step 2: Generate signed key request + const signedKeyData = await generateSignedKeyRequest( + newSigner.signer_uuid, + newSigner.public_key + ); + + // Step 3: Show QR code in access dialog for signer approval + if (signedKeyData.signer_approval_url) { + setSignerApprovalUrl(signedKeyData.signer_approval_url); + setSignersLoading(false); // Stop loading, show QR code + setDialogStep('access'); // Switch to access step to show QR + + // Step 4: Start polling for signer approval + startPolling(newSigner.signer_uuid, data.message, data.signature); + } + } else { + // If signers exist, close the dialog + // console.log('āœ… Signers already exist, closing dialog'); + setSignersLoading(false); + setShowDialog(false); + setDialogStep('signin'); + } + } catch (error) { + console.error('āŒ Error in signer flow:', error); + // On error, reset to signin step + setDialogStep('signin'); + setSignersLoading(false); + } + }; + + handleSignerFlow(); + } + }, [ + data?.message, + data?.signature, + fetchAllSigners, + createSigner, + generateSignedKeyRequest, + startPolling, + ]); + + const handleSignIn = useCallback(() => { + // console.log('šŸš€ Starting sign in flow...'); + if (isError) { + // console.log('šŸ”„ Reconnecting due to error...'); + reconnect(); + } + setDialogStep('signin'); + setShowDialog(true); + signIn(); + + // Open mobile app if on mobile and URL is available + if (url && isMobile()) { + // console.log('šŸ“± Opening mobile app:', url); + window.open(url, '_blank'); + } + }, [isError, reconnect, signIn, url]); + + const handleSignOut = useCallback(() => { + // console.log('šŸ‘‹ Signing out...'); + setShowDialog(false); + signOut(); + removeItem(STORAGE_KEY); + setStoredAuth(null); + }, [signOut]); + + // The key fix: match the original library's authentication logic exactly + const authenticated = + ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && + !!(storedAuth?.signers && storedAuth.signers.length > 0); + const userData = data || storedAuth?.userData; + + // Debug logging + // useEffect(() => { + // console.log('šŸ” Auth state:', { + // authenticated, + // isSuccess, + // validSignature, + // hasData: !!data, + // isError, + // storedAuth: !!storedAuth?.isAuthenticated, + // storedSigners: storedAuth?.signers?.length || 0, + // hasUrl: !!url, + // }); + // }, [ + // authenticated, + // isSuccess, + // validSignature, + // data, + // isError, + // storedAuth, + // url, + // ]); + + // Show loading state while nonce is being fetched or signers are loading + if (!nonce || signersLoading) { + return ( +
+
+
+ + Loading... + +
+
+ ); + } + + return ( + <> + {authenticated ? ( + + ) : ( + + )} + + {/* Unified Auth Dialog */} + {url && ( + { + setShowDialog(false); + setDialogStep('signin'); + setSignerApprovalUrl(null); + if (pollingInterval) { + clearInterval(pollingInterval); + setPollingInterval(null); + } + }} + url={url} + isError={isError} + error={error} + step={dialogStep} + isLoading={signersLoading} + signerApprovalUrl={signerApprovalUrl} + /> + )} + + {/* Debug panel (optional - can be removed in production) */} + {/* {process.env.NODE_ENV === "development" && ( +
+
Debug Info:
+
+            {JSON.stringify(
+              {
+                authenticated,
+                isSuccess,
+                validSignature,
+                hasData: !!data,
+                isPolling,
+                isError,
+                hasStoredAuth: !!storedAuth?.isAuthenticated,
+                hasUrl: !!url,
+                hasChannelToken: !!channelToken,
+              },
+              null,
+              2
+            )}
+          
+
+ )} */} + + ); +} diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index e9c0fb7..1b624c4 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -6,7 +6,7 @@ import { ShareButton } from '../Share'; import { Button } from '../Button'; import { SignIn } from '../wallet/SignIn'; import { type Haptics } from '@farcaster/frame-sdk'; -import { NeynarAuthButton } from '../NeynarAuthButton'; +import { NeynarAuthButton } from '../NeynarAuthButton/index'; /** * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. diff --git a/src/hooks/useDetectClickOutside.ts b/src/hooks/useDetectClickOutside.ts new file mode 100644 index 0000000..e6b1533 --- /dev/null +++ b/src/hooks/useDetectClickOutside.ts @@ -0,0 +1,18 @@ +import { useEffect } from 'react'; + +export function useDetectClickOutside( + ref: React.RefObject, + callback: () => void +) { + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if (ref.current && !ref.current.contains(event.target as Node)) { + callback(); + } + } + document.addEventListener('mousedown', handleClickOutside); + return () => { + document.removeEventListener('mousedown', handleClickOutside); + }; + }, [ref, callback]); +} diff --git a/src/lib/devices.ts b/src/lib/devices.ts new file mode 100644 index 0000000..f6757ec --- /dev/null +++ b/src/lib/devices.ts @@ -0,0 +1,27 @@ +function isAndroid(): boolean { + return ( + typeof navigator !== 'undefined' && /android/i.test(navigator.userAgent) + ); +} + +function isSmallIOS(): boolean { + return ( + typeof navigator !== 'undefined' && /iPhone|iPod/.test(navigator.userAgent) + ); +} + +function isLargeIOS(): boolean { + return ( + typeof navigator !== 'undefined' && + (/iPad/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) + ); +} + +function isIOS(): boolean { + return isSmallIOS() || isLargeIOS(); +} + +export function isMobile(): boolean { + return isAndroid() || isIOS(); +} diff --git a/src/lib/localStorage.ts b/src/lib/localStorage.ts new file mode 100644 index 0000000..0d86b65 --- /dev/null +++ b/src/lib/localStorage.ts @@ -0,0 +1,25 @@ +export function setItem(key: string, value: T) { + try { + localStorage.setItem(key, JSON.stringify(value)); + } catch (error) { + console.warn('Failed to save item:', error); + } +} + +export function getItem(key: string): T | null { + try { + const stored = localStorage.getItem(key); + return stored ? JSON.parse(stored) : null; + } catch (error) { + console.warn('Failed to load item:', error); + return null; + } +} + +export function removeItem(key: string) { + try { + localStorage.removeItem(key); + } catch (error) { + console.warn('Failed to remove item:', error); + } +} From bed12bd303ac4ddd7a7cbb716cc4f915fbd667c1 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 20:43:46 +0530 Subject: [PATCH 13/32] Clean up --- src/components/ui/NeynarAuthButton/index.tsx | 99 +------------------- 1 file changed, 5 insertions(+), 94 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 0a2fea0..54cb4ea 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -50,8 +50,6 @@ export function NeynarAuthButton() { // Helper function to create a signer const createSigner = useCallback(async () => { try { - // console.log('šŸ”§ Creating new signer...'); - const response = await fetch('/api/auth/signer', { method: 'POST', }); @@ -61,12 +59,10 @@ export function NeynarAuthButton() { } const signerData = await response.json(); - // console.log('āœ… Signer created:', signerData); - return signerData; } catch (error) { - // console.error('āŒ Error creating signer:', error); - throw error; + console.error('āŒ Error creating signer:', error); + // throw error; } }, []); @@ -74,8 +70,6 @@ export function NeynarAuthButton() { const generateSignedKeyRequest = useCallback( async (signerUuid: string, publicKey: string) => { try { - // console.log('šŸ”‘ Generating signed key request...'); - // Prepare request body const requestBody: { signerUuid: string; @@ -102,12 +96,11 @@ export function NeynarAuthButton() { } const data = await response.json(); - // console.log('āœ… Signed key request generated:', data); return data; } catch (error) { console.error('āŒ Error generating signed key request:', error); - throw error; + // throw error; } }, [] @@ -117,7 +110,6 @@ export function NeynarAuthButton() { const fetchAllSigners = useCallback( async (message: string, signature: string) => { try { - // console.log('ļæ½ Fetching all signers...'); setSignersLoading(true); const response = await fetch( @@ -127,11 +119,8 @@ export function NeynarAuthButton() { ); const signerData = await response.json(); - // console.log('ļæ½ Signer response:', signerData); if (response.ok) { - // console.log('āœ… Signers fetched successfully:', signerData.signers); - // Store signers in localStorage, preserving existing auth data const existingAuth = getItem(STORAGE_KEY); const updatedState: StoredAuthState = { @@ -146,11 +135,11 @@ export function NeynarAuthButton() { return signerData.signers; } else { console.error('āŒ Failed to fetch signers'); - throw new Error('Failed to fetch signers'); + // throw new Error('Failed to fetch signers'); } } catch (error) { console.error('āŒ Error fetching signers:', error); - throw error; + // throw error; } finally { setSignersLoading(false); } @@ -161,8 +150,6 @@ export function NeynarAuthButton() { // Helper function to poll signer status const startPolling = useCallback( (signerUuid: string, message: string, signature: string) => { - // console.log('ļæ½ Starting polling for signer:', signerUuid); - const interval = setInterval(async () => { try { const response = await fetch( @@ -174,10 +161,8 @@ export function NeynarAuthButton() { } const signerData = await response.json(); - // console.log('ļæ½ Signer status:', signerData.status); if (signerData.status === 'approved') { - // console.log('šŸŽ‰ Signer approved!'); clearInterval(interval); setPollingInterval(null); setShowDialog(false); @@ -230,15 +215,11 @@ export function NeynarAuthButton() { const stored = getItem(STORAGE_KEY); if (stored && stored.isAuthenticated) { setStoredAuth(stored); - if (stored.signers && stored.signers.length > 0) { - // console.log('šŸ“‚ Loaded stored signers:', stored.signers); - } } }, []); // Success callback - this is critical! const onSuccessCallback = useCallback((res: unknown) => { - // console.log('šŸŽ‰ Sign in successful!', res); const existingAuth = getItem(STORAGE_KEY); const authState: StoredAuthState = { isAuthenticated: true, @@ -279,30 +260,13 @@ export function NeynarAuthButton() { // Connect when component mounts and we have a nonce useEffect(() => { if (nonce && !channelToken) { - // console.log('šŸ”Œ Connecting with nonce:', nonce); connect(); } }, [nonce, channelToken, connect]); - // Debug logging - // useEffect(() => { - // console.log('šŸ” Auth state:', { - // isSuccess, - // validSignature, - // hasData: !!data, - // isPolling, - // isError, - // storedAuth: !!storedAuth?.isAuthenticated, - // }); - // }, [isSuccess, validSignature, data, isPolling, isError, storedAuth]); - // Handle fetching signers after successful authentication useEffect(() => { if (data?.message && data?.signature) { - // console.log('šŸ“ Got message and signature:', { - // message: data.message, - // signature: data.signature, - // }); const handleSignerFlow = async () => { try { // Ensure we have message and signature @@ -320,8 +284,6 @@ export function NeynarAuthButton() { // Check if no signers exist or if we have empty signers if (!signers || signers.length === 0) { - // console.log('šŸ”§ No signers found, creating new signer...'); - // Step 1: Create a signer const newSigner = await createSigner(); @@ -342,7 +304,6 @@ export function NeynarAuthButton() { } } else { // If signers exist, close the dialog - // console.log('āœ… Signers already exist, closing dialog'); setSignersLoading(false); setShowDialog(false); setDialogStep('signin'); @@ -367,9 +328,7 @@ export function NeynarAuthButton() { ]); const handleSignIn = useCallback(() => { - // console.log('šŸš€ Starting sign in flow...'); if (isError) { - // console.log('šŸ”„ Reconnecting due to error...'); reconnect(); } setDialogStep('signin'); @@ -378,13 +337,11 @@ export function NeynarAuthButton() { // Open mobile app if on mobile and URL is available if (url && isMobile()) { - // console.log('šŸ“± Opening mobile app:', url); window.open(url, '_blank'); } }, [isError, reconnect, signIn, url]); const handleSignOut = useCallback(() => { - // console.log('šŸ‘‹ Signing out...'); setShowDialog(false); signOut(); removeItem(STORAGE_KEY); @@ -397,28 +354,6 @@ export function NeynarAuthButton() { !!(storedAuth?.signers && storedAuth.signers.length > 0); const userData = data || storedAuth?.userData; - // Debug logging - // useEffect(() => { - // console.log('šŸ” Auth state:', { - // authenticated, - // isSuccess, - // validSignature, - // hasData: !!data, - // isError, - // storedAuth: !!storedAuth?.isAuthenticated, - // storedSigners: storedAuth?.signers?.length || 0, - // hasUrl: !!url, - // }); - // }, [ - // authenticated, - // isSuccess, - // validSignature, - // data, - // isError, - // storedAuth, - // url, - // ]); - // Show loading state while nonce is being fetched or signers are loading if (!nonce || signersLoading) { return ( @@ -482,30 +417,6 @@ export function NeynarAuthButton() { signerApprovalUrl={signerApprovalUrl} /> )} - - {/* Debug panel (optional - can be removed in production) */} - {/* {process.env.NODE_ENV === "development" && ( -
-
Debug Info:
-
-            {JSON.stringify(
-              {
-                authenticated,
-                isSuccess,
-                validSignature,
-                hasData: !!data,
-                isPolling,
-                isError,
-                hasStoredAuth: !!storedAuth?.isAuthenticated,
-                hasUrl: !!url,
-                hasChannelToken: !!channelToken,
-              },
-              null,
-              2
-            )}
-          
-
- )} */} ); } From 1d736d823eac500c407d325d459d53e86bfdce79 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 7 Jul 2025 21:47:40 +0530 Subject: [PATCH 14/32] Ask SPONSOR_SIGNER --- bin/init.js | 34 ++++++++++++++++++++++++++++++++++ scripts/build.js | 7 ++++++- scripts/deploy.js | 33 ++++++++++++++++++++++++++++++++- 3 files changed, 72 insertions(+), 2 deletions(-) diff --git a/bin/init.js b/bin/init.js index 659f36c..1534beb 100644 --- a/bin/init.js +++ b/bin/init.js @@ -221,6 +221,8 @@ export async function init(projectName = null, autoAcceptDefaults = false) { useWallet: true, useTunnel: true, enableAnalytics: true, + seedPhrase: null, + sponsorSigner: false, }; } else { // If autoAcceptDefaults is false but we have a projectName, we still need to ask for other options @@ -347,6 +349,34 @@ export async function init(projectName = null, autoAcceptDefaults = false) { }, ]); answers.enableAnalytics = analyticsAnswer.enableAnalytics; + + // Ask about SEED_PHRASE + const seedPhraseAnswer = await inquirer.prompt([ + { + type: 'password', + name: 'seedPhrase', + message: + 'āš ļø If SEED_PHRASE is not provided, you will not be able to use Sign In With Neynar.\n\n' + + 'Enter your SEED_PHRASE (or press enter to skip):', + default: null, + }, + ]); + answers.seedPhrase = seedPhraseAnswer.seedPhrase; + + // Ask about sponsor signer if SEED_PHRASE is provided + if (answers.seedPhrase) { + const sponsorSignerAnswer = await inquirer.prompt([ + { + type: 'confirm', + name: 'sponsorSigner', + message: + 'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' + + 'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.', + default: false, + }, + ]); + answers.sponsorSigner = sponsorSignerAnswer.sponsorSigner; + } } const finalProjectName = answers.projectName; @@ -598,6 +628,10 @@ export async function init(projectName = null, autoAcceptDefaults = false) { '\nāš ļø Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID' ); } + if (answers.seedPhrase) { + fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`); + fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="${answers.sponsorSigner}"`); + } fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`); fs.unlinkSync(envExamplePath); diff --git a/scripts/build.js b/scripts/build.js index b680399..a4580ce 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -65,7 +65,7 @@ async function loadEnvLocal() { let newEnvContent = envContent; for (const [key, value] of Object.entries(localEnv)) { - if (key !== 'SEED_PHRASE') { + if (key !== 'SEED_PHRASE' && key !== 'SPONSOR_SIGNER') { // Update process.env process.env[key] = value; // Add to .env content if not already there @@ -87,6 +87,9 @@ async function loadEnvLocal() { if (localEnv.SEED_PHRASE) { process.env.SEED_PHRASE = localEnv.SEED_PHRASE; } + if (localEnv.SPONSOR_SIGNER) { + process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; + } } } catch (error) { // Error reading .env.local, which is fine @@ -370,6 +373,8 @@ async function main() { [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), ...(neynarClientId ? [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), + ...(process.env.SPONSOR_SIGNER ? + [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] : []), // FID (if it exists in current env) ...(process.env.FID ? [`FID="${process.env.FID}"`] : []), diff --git a/scripts/deploy.js b/scripts/deploy.js index ca53f97..1b4c0f8 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -115,6 +115,7 @@ async function loadEnvLocal() { const allowedVars = [ 'SEED_PHRASE', + 'SPONSOR_SIGNER', 'NEXT_PUBLIC_MINI_APP_NAME', 'NEXT_PUBLIC_MINI_APP_DESCRIPTION', 'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY', @@ -131,7 +132,7 @@ async function loadEnvLocal() { for (const [key, value] of Object.entries(localEnv)) { if (allowedVars.includes(key)) { process.env[key] = value; - if (key !== 'SEED_PHRASE' && !envContent.includes(`${key}=`)) { + if (key !== 'SEED_PHRASE' && key !== 'SPONSOR_SIGNER' && !envContent.includes(`${key}=`)) { newEnvContent += `${key}="${value}"\n`; } } @@ -224,6 +225,35 @@ async function checkRequiredEnvVars() { } else { console.log('ā„¹ļø Seed phrase will only be used for this deployment'); } + + // Ask about sponsor signer if SEED_PHRASE is provided + if (!process.env.SPONSOR_SIGNER) { + const { sponsorSigner } = await inquirer.prompt([ + { + type: 'confirm', + name: 'sponsorSigner', + message: + 'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' + + 'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.', + default: false, + }, + ]); + + process.env.SPONSOR_SIGNER = sponsorSigner.toString(); + + if (storeSeedPhrase) { + fs.appendFileSync('.env.local', `\nSPONSOR_SIGNER="${sponsorSigner}"`); + console.log('āœ… Sponsor signer preference stored in .env.local'); + } + } + } + } + + // Load SPONSOR_SIGNER from .env.local if SEED_PHRASE exists but SPONSOR_SIGNER doesn't + if (process.env.SEED_PHRASE && !process.env.SPONSOR_SIGNER && fs.existsSync('.env.local')) { + const localEnv = dotenv.parse(fs.readFileSync('.env.local')); + if (localEnv.SPONSOR_SIGNER) { + process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; } } } @@ -585,6 +615,7 @@ async function deployToVercel(useGitHub = false) { ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }), + ...(process.env.SPONSOR_SIGNER && { SPONSOR_SIGNER: process.env.SPONSOR_SIGNER }), ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }), ...Object.fromEntries( From 485c1906952b456aec9dc629bfdb64a46b142266 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Tue, 8 Jul 2025 16:08:02 +0530 Subject: [PATCH 15/32] fix mobile redirect --- .../ui/NeynarAuthButton/AuthDialog.tsx | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 8c8f984..3b38f84 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -193,24 +193,17 @@ export function AuthDialog({ {content.showOpenButton && content.qrUrl && ( )} From 822db9101f6f8c47436e459393598c46691213b5 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Tue, 8 Jul 2025 19:16:39 +0530 Subject: [PATCH 16/32] If within farcaster redirect skip grant access screen --- .../ui/NeynarAuthButton/AuthDialog.tsx | 16 +++++++++------- src/components/ui/NeynarAuthButton/index.tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 3b38f84..2fc64d3 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -149,9 +149,6 @@ export function AuthDialog({
{error?.message || 'Unknown error, please try again.'}
-
) : (
@@ -195,10 +192,15 @@ export function AuthDialog({
{isError ? ( -
-
+
+
{error?.message || 'Unknown error, please try again.'}
) : ( -
-
+
+
{typeof content.description === 'string' ? ( -

+

{content.description}

) : ( @@ -162,23 +162,23 @@ export function AuthDialog({ )}
-
+
{content.showQR && content.qrUrl ? ( -
+
{/* eslint-disable-next-line @next/next/no-img-element */} QR Code
) : step === 'loading' || isLoading ? ( -
-
-
- +
+
+
+ {step === 'loading' ? 'Setting up access...' : 'Loading...'} @@ -204,7 +204,7 @@ export function AuthDialog({ '_blank' ) } - className='btn btn-outline flex items-center justify-center gap-2 w-full' + className="btn btn-outline flex items-center justify-center gap-2 w-full" > I'm using my phone → diff --git a/src/components/ui/NeynarAuthButton/ProfileButton.tsx b/src/components/ui/NeynarAuthButton/ProfileButton.tsx index daec476..bcf1ca7 100644 --- a/src/components/ui/NeynarAuthButton/ProfileButton.tsx +++ b/src/components/ui/NeynarAuthButton/ProfileButton.tsx @@ -20,7 +20,7 @@ export function ProfileButton({ const pfpUrl = userData?.pfpUrl ?? 'https://farcaster.xyz/avatar.png'; return ( -
+
{showDropdown && ( -
+
)} {/* Unified Auth Dialog */} - {url && ( + { { setShowDialog(false); setDialogStep('signin'); @@ -423,7 +602,7 @@ export function NeynarAuthButton() { isLoading={signersLoading} signerApprovalUrl={signerApprovalUrl} /> - )} + } ); } From 96bb45ede0d9538fadef8de7a9db54f0ee8673a6 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Thu, 10 Jul 2025 18:45:07 +0530 Subject: [PATCH 20/32] Seperate SIWN and SIWF --- src/auth.ts | 118 ++++++++++++++++++- src/components/ui/NeynarAuthButton/index.tsx | 19 ++- src/components/ui/wallet/SignIn.tsx | 45 +++---- 3 files changed, 156 insertions(+), 26 deletions(-) diff --git a/src/auth.ts b/src/auth.ts index 4d06956..a8e6ca1 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -6,8 +6,15 @@ declare module 'next-auth' { interface Session { user: { fid: number; + provider?: string; + username?: string; }; } + + interface User { + provider?: string; + username?: string; + } } function getDomainFromUrl(urlString: string | undefined): string { @@ -29,6 +36,7 @@ export const authOptions: AuthOptions = { // Configure one or more authentication providers providers: [ CredentialsProvider({ + id: 'farcaster', name: 'Sign in with Farcaster', credentials: { message: { @@ -62,9 +70,7 @@ export const authOptions: AuthOptions = { }, }, async authorize(credentials, req) { - const csrfToken = req?.body?.csrfToken; - - const nonce = credentials?.nonce || csrfToken; + const nonce = req?.body?.csrfToken; if (!nonce) { console.error('No nonce or CSRF token provided'); @@ -91,17 +97,123 @@ export const authOptions: AuthOptions = { return { id: fid.toString(), + name: credentials?.name || `User ${fid}`, + image: credentials?.pfp || null, + provider: 'farcaster', }; }, }), + CredentialsProvider({ + id: 'neynar', + name: 'Sign in with Neynar', + credentials: { + message: { + label: 'Message', + type: 'text', + placeholder: '0x0', + }, + signature: { + label: 'Signature', + type: 'text', + placeholder: '0x0', + }, + nonce: { + label: 'Nonce', + type: 'text', + placeholder: 'Custom nonce (optional)', + }, + fid: { + label: 'FID', + type: 'text', + placeholder: '0', + }, + username: { + label: 'Username', + type: 'text', + placeholder: 'username', + }, + displayName: { + label: 'Display Name', + type: 'text', + placeholder: 'Display Name', + }, + pfpUrl: { + label: 'Profile Picture URL', + type: 'text', + placeholder: 'https://...', + }, + }, + async authorize(credentials) { + const nonce = credentials?.nonce; + + if (!nonce) { + console.error('No nonce or CSRF token provided for Neynar auth'); + return null; + } + + // For Neynar, we can use a different validation approach + // This could involve validating against Neynar's API or using their SDK + try { + // Validate the signature using Farcaster's auth client (same as Farcaster provider) + const appClient = createAppClient({ + ethereum: viemConnector(), + }); + + const domain = getDomainFromUrl(process.env.NEXTAUTH_URL); + + const verifyResponse = await appClient.verifySignInMessage({ + message: credentials?.message as string, + signature: credentials?.signature as `0x${string}`, + domain, + nonce, + }); + + const { success, fid } = verifyResponse; + + if (!success) { + return null; + } + + // Validate that the provided FID matches the verified FID + if (credentials?.fid && parseInt(credentials.fid) !== fid) { + console.error('FID mismatch in Neynar auth'); + return null; + } + + return { + id: fid.toString(), + name: + credentials?.displayName || + credentials?.username || + `User ${fid}`, + image: credentials?.pfpUrl || null, + provider: 'neynar', + username: credentials?.username || undefined, + }; + } catch (error) { + console.error('Error in Neynar auth:', error); + return null; + } + }, + }), ], callbacks: { session: async ({ session, token }) => { if (session?.user) { session.user.fid = parseInt(token.sub ?? ''); + // Add provider information to session + session.user.provider = token.provider as string; + session.user.username = token.username as string; } return session; }, + jwt: async ({ token, user }) => { + if (user) { + token.provider = user.provider; + token.username = user.username; + } + return token; + }, }, cookies: { sessionToken: { diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 874ea81..1fec0b1 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -13,9 +13,18 @@ import { useMiniApp } from '@neynar/react'; import { signIn as backendSignIn, signOut as backendSignOut, + useSession, } from 'next-auth/react'; import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk'; +type User = { + fid: number; + username: string; + display_name: string; + pfp_url: string; + // Add other user properties as needed +}; + const STORAGE_KEY = 'neynar_authenticated_user'; const FARCASTER_FID = 9152; @@ -90,6 +99,7 @@ export function NeynarAuthButton() { const [storedAuth, setStoredAuth] = useState(null); const [signersLoading, setSignersLoading] = useState(false); const { context } = useMiniApp(); + const { data: session } = useSession(); // New state for unified dialog flow const [showDialog, setShowDialog] = useState(false); const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>( @@ -468,7 +478,7 @@ export function NeynarAuthButton() { nonce: nonce, }; - const nextAuthResult = await backendSignIn('credentials', signInData); + const nextAuthResult = await backendSignIn('neynar', signInData); if (nextAuthResult?.ok) { setMessage(result.message); setSignature(result.signature); @@ -503,7 +513,10 @@ export function NeynarAuthButton() { setSignersLoading(true); if (useBackendFlow) { - await backendSignOut({ redirect: false }); + // Only sign out from NextAuth if the current session is from Neynar provider + if (session?.user?.provider === 'neynar') { + await backendSignOut({ redirect: false }); + } } else { frontendSignOut(); } @@ -528,7 +541,7 @@ export function NeynarAuthButton() { } finally { setSignersLoading(false); } - }, [useBackendFlow, frontendSignOut, pollingInterval]); + }, [useBackendFlow, frontendSignOut, pollingInterval, session]); // The key fix: match the original library's authentication logic exactly const authenticated = diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index 890ab9f..3e489d7 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -77,7 +77,7 @@ export function SignIn() { const nonce = await getNonce(); const result = await sdk.actions.signIn({ nonce }); setSignInResult(result); - await signIn('credentials', { + await signIn('farcaster', { message: result.message, signature: result.signature, redirect: false, @@ -96,41 +96,46 @@ export function SignIn() { /** * Handles the sign-out process. * - * This function clears the NextAuth session and resets the local - * sign-in result state to complete the sign-out flow. + * This function clears the NextAuth session only if the current session + * is using the Farcaster provider, and resets the local sign-in result state. * * @returns Promise */ const handleSignOut = useCallback(async () => { try { setAuthState((prev) => ({ ...prev, signingOut: true })); - await signOut({ redirect: false }); + // Only sign out if the current session is from Farcaster provider + if (session?.user?.provider === 'farcaster') { + await signOut({ redirect: false }); + } setSignInResult(undefined); } finally { setAuthState((prev) => ({ ...prev, signingOut: false })); } - }, []); + }, [session]); // --- Render --- return ( <> {/* Authentication Buttons */} - {status !== 'authenticated' && ( + {(status !== 'authenticated' || + session?.user?.provider !== 'farcaster') && ( )} - {status === 'authenticated' && ( - - )} + {status === 'authenticated' && + session?.user?.provider === 'farcaster' && ( + + )} {/* Session Information */} {session && ( -
-
Session
-
+
+
Session
+
{JSON.stringify(session, null, 2)}
@@ -138,17 +143,17 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && ( -
-
SIWF Result
-
{signInFailure}
+
+
SIWF Result
+
{signInFailure}
)} {/* Success Result Display */} {signInResult && !authState.signingIn && ( -
-
SIWF Result
-
+
+
SIWF Result
+
{JSON.stringify(signInResult, null, 2)}
From 797c5b71546c7498e4b32c14ef36bc6b4703ef87 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Thu, 10 Jul 2025 21:25:55 +0530 Subject: [PATCH 21/32] Cleanup --- src/app/api/auth/session-signers/route.ts | 99 ++++++++ src/app/api/auth/update-session/route.ts | 46 ++++ src/auth.ts | 236 +++++++++++++++++-- src/components/ui/NeynarAuthButton/index.tsx | 201 ++++++++++------ src/components/ui/wallet/SignIn.tsx | 16 +- 5 files changed, 495 insertions(+), 103 deletions(-) create mode 100644 src/app/api/auth/session-signers/route.ts create mode 100644 src/app/api/auth/update-session/route.ts diff --git a/src/app/api/auth/session-signers/route.ts b/src/app/api/auth/session-signers/route.ts new file mode 100644 index 0000000..0d23d48 --- /dev/null +++ b/src/app/api/auth/session-signers/route.ts @@ -0,0 +1,99 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '~/auth'; +import { getNeynarClient } from '~/lib/neynar'; + +export async function GET(request: Request) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.fid) { + return NextResponse.json( + { error: 'No authenticated session found' }, + { status: 401 } + ); + } + + const { searchParams } = new URL(request.url); + const message = searchParams.get('message'); + const signature = searchParams.get('signature'); + + if (!message || !signature) { + return NextResponse.json( + { error: 'Message and signature are required' }, + { status: 400 } + ); + } + + const client = getNeynarClient(); + const data = await client.fetchSigners({ message, signature }); + const signers = data.signers; + + // Fetch user data if signers exist + let user = null; + if (signers && signers.length > 0) { + try { + const userResponse = await fetch( + `${process.env.NEXTAUTH_URL}/api/users?fids=${signers[0].fid}` + ); + if (userResponse.ok) { + const userDataResponse = await userResponse.json(); + user = userDataResponse.users?.[0] || null; + } + } catch (error) { + console.error('Error fetching user data:', error); + } + } + + return NextResponse.json({ + signers, + user, + }); + } catch (error) { + console.error('Error in session-signers API:', error); + return NextResponse.json( + { error: 'Failed to fetch signers' }, + { status: 500 } + ); + } +} + +export async function POST(request: Request) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.fid) { + return NextResponse.json( + { error: 'No authenticated session found' }, + { status: 401 } + ); + } + + const body = await request.json(); + const { message, signature, signers, user } = body; + + if (!message || !signature || !signers) { + return NextResponse.json( + { error: 'Message, signature, and signers are required' }, + { status: 400 } + ); + } + + // Since we can't directly modify the session token here, + // we'll return the data and let the client trigger a session update + // The client will need to call getSession() to refresh the session + + return NextResponse.json({ + success: true, + message: 'Session data prepared for update', + signers, + user, + }); + } catch (error) { + console.error('Error updating session signers:', error); + return NextResponse.json( + { error: 'Failed to update session' }, + { status: 500 } + ); + } +} diff --git a/src/app/api/auth/update-session/route.ts b/src/app/api/auth/update-session/route.ts new file mode 100644 index 0000000..db4b4fc --- /dev/null +++ b/src/app/api/auth/update-session/route.ts @@ -0,0 +1,46 @@ +import { NextResponse } from 'next/server'; +import { getServerSession } from 'next-auth'; +import { authOptions } from '~/auth'; + +export async function POST(request: Request) { + try { + const session = await getServerSession(authOptions); + + if (!session?.user?.fid) { + return NextResponse.json( + { error: 'No authenticated session found' }, + { status: 401 } + ); + } + + const body = await request.json(); + const { signers, user } = body; + + if (!signers || !user) { + return NextResponse.json( + { error: 'Signers and user are required' }, + { status: 400 } + ); + } + + // For NextAuth to update the session, we need to trigger the JWT callback + // This is typically done by calling the session endpoint with updated data + // However, we can't directly modify the session token from here + + // Instead, we'll store the data temporarily and let the client refresh the session + // The session will be updated when the JWT callback is triggered + + return NextResponse.json({ + success: true, + message: 'Session update prepared', + signers, + user, + }); + } catch (error) { + console.error('Error preparing session update:', error); + return NextResponse.json( + { error: 'Failed to prepare session update' }, + { status: 500 } + ); + } +} diff --git a/src/auth.ts b/src/auth.ts index a8e6ca1..c3345fb 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -4,16 +4,198 @@ import { createAppClient, viemConnector } from '@farcaster/auth-client'; declare module 'next-auth' { interface Session { - user: { + provider?: string; + user?: { fid: number; - provider?: string; + object?: 'user'; username?: string; + display_name?: string; + pfp_url?: string; + custody_address?: string; + profile?: { + bio: { + text: string; + mentioned_profiles?: Array<{ + object: 'user_dehydrated'; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + }>; + mentioned_profiles_ranges?: Array<{ + start: number; + end: number; + }>; + }; + location?: { + latitude: number; + longitude: number; + address: { + city: string; + state: string; + country: string; + country_code: string; + }; + }; + }; + follower_count?: number; + following_count?: number; + verifications?: string[]; + verified_addresses?: { + eth_addresses: string[]; + sol_addresses: string[]; + primary: { + eth_address: string; + sol_address: string; + }; + }; + verified_accounts?: Array>; + power_badge?: boolean; + url?: string; + experimental?: { + neynar_user_score: number; + deprecation_notice: string; + }; + score?: number; }; + signers?: { + object: 'signer'; + signer_uuid: string; + public_key: string; + status: 'approved'; + fid: number; + }[]; } interface User { provider?: string; - username?: string; + signers?: Array<{ + object: 'signer'; + signer_uuid: string; + public_key: string; + status: 'approved'; + fid: number; + }>; + user?: { + object: 'user'; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + profile: { + bio: { + text: string; + mentioned_profiles?: Array<{ + object: 'user_dehydrated'; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + }>; + mentioned_profiles_ranges?: Array<{ + start: number; + end: number; + }>; + }; + location?: { + latitude: number; + longitude: number; + address: { + city: string; + state: string; + country: string; + country_code: string; + }; + }; + }; + follower_count: number; + following_count: number; + verifications: string[]; + verified_addresses: { + eth_addresses: string[]; + sol_addresses: string[]; + primary: { + eth_address: string; + sol_address: string; + }; + }; + verified_accounts: Array>; + power_badge: boolean; + url?: string; + experimental?: { + neynar_user_score: number; + deprecation_notice: string; + }; + score: number; + }; + } + + interface JWT { + provider?: string; + signers?: Array<{ + object: 'signer'; + signer_uuid: string; + public_key: string; + status: 'approved'; + fid: number; + }>; + user?: { + object: 'user'; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + profile: { + bio: { + text: string; + mentioned_profiles?: Array<{ + object: 'user_dehydrated'; + fid: number; + username: string; + display_name: string; + pfp_url: string; + custody_address: string; + }>; + mentioned_profiles_ranges?: Array<{ + start: number; + end: number; + }>; + }; + location?: { + latitude: number; + longitude: number; + address: { + city: string; + state: string; + country: string; + country_code: string; + }; + }; + }; + follower_count: number; + following_count: number; + verifications: string[]; + verified_addresses: { + eth_addresses: string[]; + sol_addresses: string[]; + primary: { + eth_address: string; + sol_address: string; + }; + }; + verified_accounts?: Array>; + power_badge?: boolean; + url?: string; + experimental?: { + neynar_user_score: number; + deprecation_notice: string; + }; + score?: number; + }; } } @@ -127,20 +309,15 @@ export const authOptions: AuthOptions = { type: 'text', placeholder: '0', }, - username: { - label: 'Username', + signers: { + label: 'Signers', type: 'text', - placeholder: 'username', + placeholder: 'JSON string of signers', }, - displayName: { - label: 'Display Name', + user: { + label: 'User Data', type: 'text', - placeholder: 'Display Name', - }, - pfpUrl: { - label: 'Profile Picture URL', - type: 'text', - placeholder: 'https://...', + placeholder: 'JSON string of user data', }, }, async authorize(credentials) { @@ -182,13 +359,11 @@ export const authOptions: AuthOptions = { return { id: fid.toString(), - name: - credentials?.displayName || - credentials?.username || - `User ${fid}`, - image: credentials?.pfpUrl || null, provider: 'neynar', - username: credentials?.username || undefined, + signers: credentials?.signers + ? JSON.parse(credentials.signers) + : undefined, + user: credentials?.user ? JSON.parse(credentials.user) : undefined, }; } catch (error) { console.error('Error in Neynar auth:', error); @@ -199,18 +374,27 @@ export const authOptions: AuthOptions = { ], callbacks: { session: async ({ session, token }) => { - if (session?.user) { - session.user.fid = parseInt(token.sub ?? ''); - // Add provider information to session - session.user.provider = token.provider as string; - session.user.username = token.username as string; + // Set provider at the root level + session.provider = token.provider as string; + + if (token.provider === 'farcaster') { + // For Farcaster, simple structure + session.user = { + fid: parseInt(token.sub ?? ''), + }; + } else if (token.provider === 'neynar') { + // For Neynar, use full user data structure from user + session.user = token.user as typeof session.user; + session.signers = token.signers as typeof session.signers; } + return session; }, jwt: async ({ token, user }) => { if (user) { token.provider = user.provider; - token.username = user.username; + token.signers = user.signers; + token.user = user.user; } return token; }, diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 1fec0b1..a4d18a3 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -137,6 +137,36 @@ export function NeynarAuthButton() { } }, []); + // Helper function to update session with signers (backend flow only) + const updateSessionWithSigners = useCallback( + async ( + signers: StoredAuthState['signers'], + user: StoredAuthState['user'] + ) => { + if (!useBackendFlow) return; + + try { + // For backend flow, we need to sign in again with the additional data + if (message && signature) { + 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) { + console.error('āŒ Error updating session with signers:', error); + } + }, + [useBackendFlow, message, signature, nonce] + ); + // Helper function to fetch user data from Neynar API const fetchUserData = useCallback( async (fid: number): Promise => { @@ -201,33 +231,51 @@ export function NeynarAuthButton() { try { setSignersLoading(true); - const response = await fetch( - `/api/auth/signers?message=${encodeURIComponent( - message - )}&signature=${signature}` - ); + const endpoint = useBackendFlow + ? `/api/auth/session-signers?message=${encodeURIComponent( + message + )}&signature=${signature}` + : `/api/auth/signers?message=${encodeURIComponent( + message + )}&signature=${signature}`; + const response = await fetch(endpoint); const signerData = await response.json(); if (response.ok) { - let user: StoredAuthState['user'] | null = null; + if (useBackendFlow) { + // For backend flow, update session with signers + if (signerData.signers && signerData.signers.length > 0) { + const user = + signerData.user || + (await fetchUserData(signerData.signers[0].fid)); + await updateSessionWithSigners(signerData.signers, user); + } + return signerData.signers; + } else { + // For frontend flow, store in localStorage + let user: StoredAuthState['user'] | null = null; - if (signerData.signers && signerData.signers.length > 0) { - user = await fetchUserData(signerData.signers[0].fid); + if (signerData.signers && signerData.signers.length > 0) { + const fetchedUser = (await fetchUserData( + signerData.signers[0].fid + )) as StoredAuthState['user']; + user = fetchedUser; + } + + // Store signers in localStorage, preserving existing auth data + const existingAuth = getItem(STORAGE_KEY); + const updatedState: StoredAuthState = { + ...existingAuth, + isAuthenticated: !!user, + signers: signerData.signers || [], + user, + }; + setItem(STORAGE_KEY, updatedState); + setStoredAuth(updatedState); + + return signerData.signers; } - - // Store signers in localStorage, preserving existing auth data - const existingAuth = getItem(STORAGE_KEY); - const updatedState: StoredAuthState = { - ...existingAuth, - isAuthenticated: !!user, - signers: signerData.signers || [], - user, - }; - setItem(STORAGE_KEY, updatedState); - setStoredAuth(updatedState); - - return signerData.signers; } else { console.error('āŒ Failed to fetch signers'); // throw new Error('Failed to fetch signers'); @@ -239,7 +287,7 @@ export function NeynarAuthButton() { setSignersLoading(false); } }, - [] + [useBackendFlow, fetchUserData, updateSessionWithSigners] ); // Helper function to poll signer status @@ -305,26 +353,34 @@ export function NeynarAuthButton() { generateNonce(); }, []); - // Load stored auth state on mount + // Load stored auth state on mount (only for frontend flow) useEffect(() => { - const stored = getItem(STORAGE_KEY); - if (stored && stored.isAuthenticated) { - setStoredAuth(stored); + if (!useBackendFlow) { + const stored = getItem(STORAGE_KEY); + if (stored && stored.isAuthenticated) { + setStoredAuth(stored); + } } - }, []); + }, [useBackendFlow]); // Success callback - this is critical! - const onSuccessCallback = useCallback((res: unknown) => { - const existingAuth = getItem(STORAGE_KEY); - const authState: StoredAuthState = { - isAuthenticated: true, - user: res as StoredAuthState['user'], - signers: existingAuth?.signers || [], // Preserve existing signers - }; - setItem(STORAGE_KEY, authState); - setStoredAuth(authState); - // setShowDialog(false); - }, []); + const onSuccessCallback = useCallback( + (res: unknown) => { + if (!useBackendFlow) { + // Only handle localStorage for frontend flow + const existingAuth = getItem(STORAGE_KEY); + const authState: StoredAuthState = { + isAuthenticated: true, + user: res as StoredAuthState['user'], + signers: existingAuth?.signers || [], // Preserve existing signers + }; + setItem(STORAGE_KEY, authState); + setStoredAuth(authState); + } + // For backend flow, the session will be handled by NextAuth + }, + [useBackendFlow] + ); // Error callback const onErrorCallback = useCallback((error?: Error | null) => { @@ -368,12 +424,6 @@ export function NeynarAuthButton() { if (message && signature) { const handleSignerFlow = async () => { try { - // // Ensure we have message and signature - // if (!message || !signature) { - // console.error('āŒ Missing message or signature'); - // return; - // } - // Step 1: Change to loading state setDialogStep('loading'); setSignersLoading(true); @@ -403,10 +453,13 @@ export function NeynarAuthButton() { setDebugState('Setting signer approval URL...'); setSignerApprovalUrl(signedKeyData.signer_approval_url); setSignersLoading(false); // Stop loading, show QR code - if ( - context?.client?.platformType === 'mobile' && - context?.client?.clientFid === FARCASTER_FID - ) { + // Check if we're in a mobile context + const clientContext = context?.client as Record; + const isMobileContext = + clientContext?.platformType === 'mobile' && + clientContext?.clientFid === FARCASTER_FID; + + if (isMobileContext) { setDebugState('Opening mobile app...'); setShowDialog(false); await sdk.actions.openUrl( @@ -418,16 +471,11 @@ export function NeynarAuthButton() { } else { setDebugState( 'Opening access dialog...' + - ` ${context?.client?.platformType}` + - ` ${context?.client?.clientFid}` + ` ${clientContext?.platformType}` + + ` ${clientContext?.clientFid}` ); setDialogStep('access'); setShowDialog(true); - setDebugState( - 'Opening access dialog...2' + - ` ${dialogStep}` + - ` ${showDialog}` - ); } // Step 4: Start polling for signer approval @@ -514,15 +562,17 @@ export function NeynarAuthButton() { if (useBackendFlow) { // Only sign out from NextAuth if the current session is from Neynar provider - if (session?.user?.provider === 'neynar') { + if (session?.provider === 'neynar') { await backendSignOut({ redirect: false }); } } else { + // Frontend flow sign out frontendSignOut(); + removeItem(STORAGE_KEY); + setStoredAuth(null); } - removeItem(STORAGE_KEY); - setStoredAuth(null); + // Common cleanup for both flows setShowDialog(false); setDialogStep('signin'); setSignerApprovalUrl(null); @@ -543,15 +593,27 @@ export function NeynarAuthButton() { } }, [useBackendFlow, frontendSignOut, pollingInterval, session]); - // The key fix: match the original library's authentication logic exactly - const authenticated = - ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && - !!(storedAuth?.signers && storedAuth.signers.length > 0); - const userData = { - fid: storedAuth?.user?.fid, - username: storedAuth?.user?.username || '', - pfpUrl: storedAuth?.user?.pfp_url || '', - }; + const authenticated = useBackendFlow + ? !!( + session?.provider === 'neynar' && + session?.user?.fid && + session?.signers && + session.signers.length > 0 + ) + : ((isSuccess && validSignature) || storedAuth?.isAuthenticated) && + !!(storedAuth?.signers && storedAuth.signers.length > 0); + + const userData = useBackendFlow + ? { + fid: session?.user?.fid, + username: session?.user?.username || '', + pfpUrl: session?.user?.pfp_url || '', + } + : { + fid: storedAuth?.user?.fid, + username: storedAuth?.user?.username || '', + pfpUrl: storedAuth?.user?.pfp_url || '', + }; // Show loading state while nonce is being fetched or signers are loading if (!nonce || signersLoading) { @@ -589,12 +651,15 @@ export function NeynarAuthButton() { ) : ( <> - {debugState || 'Sign in with Neynar'} + Sign in with Neynar )} )} +

LocalStorage state

+ {window && JSON.stringify(window.localStorage.getItem(STORAGE_KEY))} + {/* Unified Auth Dialog */} { ({ ...prev, signingOut: true })); // Only sign out if the current session is from Farcaster provider - if (session?.user?.provider === 'farcaster') { + if (session?.provider === 'farcaster') { await signOut({ redirect: false }); } setSignInResult(undefined); @@ -118,18 +118,16 @@ export function SignIn() { return ( <> {/* Authentication Buttons */} - {(status !== 'authenticated' || - session?.user?.provider !== 'farcaster') && ( + {(status !== 'authenticated' || session?.provider !== 'farcaster') && ( )} - {status === 'authenticated' && - session?.user?.provider === 'farcaster' && ( - - )} + {status === 'authenticated' && session?.provider === 'farcaster' && ( + + )} {/* Session Information */} {session && ( From 7b431677dcbad9355ceb8e6ecba088ee15ec04f0 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 02:35:25 +0530 Subject: [PATCH 22/32] fix 401 and cleanup --- src/app/api/auth/session-signers/route.ts | 9 ----- src/components/ui/NeynarAuthButton/index.tsx | 35 +++----------------- 2 files changed, 5 insertions(+), 39 deletions(-) diff --git a/src/app/api/auth/session-signers/route.ts b/src/app/api/auth/session-signers/route.ts index 0d23d48..9d41b0d 100644 --- a/src/app/api/auth/session-signers/route.ts +++ b/src/app/api/auth/session-signers/route.ts @@ -5,15 +5,6 @@ import { getNeynarClient } from '~/lib/neynar'; export async function GET(request: Request) { try { - const session = await getServerSession(authOptions); - - if (!session?.user?.fid) { - return NextResponse.json( - { error: 'No authenticated session found' }, - { status: 401 } - ); - } - const { searchParams } = new URL(request.url); const message = searchParams.get('message'); const signature = searchParams.get('signature'); diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index a4d18a3..c8f1492 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -113,7 +113,6 @@ export function NeynarAuthButton() { ); const [message, setMessage] = useState(null); const [signature, setSignature] = useState(null); - const [debugState, setDebugState] = useState(null); // Determine which flow to use based on context const useBackendFlow = context !== undefined; @@ -264,9 +263,7 @@ export function NeynarAuthButton() { } // Store signers in localStorage, preserving existing auth data - const existingAuth = getItem(STORAGE_KEY); const updatedState: StoredAuthState = { - ...existingAuth, isAuthenticated: !!user, signers: signerData.signers || [], user, @@ -365,13 +362,15 @@ export function NeynarAuthButton() { // Success callback - this is critical! const onSuccessCallback = useCallback( - (res: unknown) => { + async (res: unknown) => { if (!useBackendFlow) { // Only handle localStorage for frontend flow const existingAuth = getItem(STORAGE_KEY); + const user = await fetchUserData(res.fid); const authState: StoredAuthState = { + ...existingAuth, isAuthenticated: true, - user: res as StoredAuthState['user'], + user: user as StoredAuthState['user'], signers: existingAuth?.signers || [], // Preserve existing signers }; setItem(STORAGE_KEY, authState); @@ -424,35 +423,24 @@ export function NeynarAuthButton() { if (message && signature) { const handleSignerFlow = async () => { try { - // Step 1: Change to loading state - setDialogStep('loading'); - setSignersLoading(true); - // First, fetch existing signers const signers = await fetchAllSigners(message, signature); - - setDebugState('Fetched signers...'); + setSignersLoading(true); // Check if no signers exist or if we have empty signers if (!signers || signers.length === 0) { // Step 1: Create a signer const newSigner = await createSigner(); - setDebugState('Created new signer...'); - // Step 2: Generate signed key request const signedKeyData = await generateSignedKeyRequest( newSigner.signer_uuid, newSigner.public_key ); - setDebugState('Generated signed key request...'); - // Step 3: Show QR code in access dialog for signer approval if (signedKeyData.signer_approval_url) { - setDebugState('Setting signer approval URL...'); setSignerApprovalUrl(signedKeyData.signer_approval_url); - setSignersLoading(false); // Stop loading, show QR code // Check if we're in a mobile context const clientContext = context?.client as Record; const isMobileContext = @@ -460,7 +448,6 @@ export function NeynarAuthButton() { clientContext?.clientFid === FARCASTER_FID; if (isMobileContext) { - setDebugState('Opening mobile app...'); setShowDialog(false); await sdk.actions.openUrl( signedKeyData.signer_approval_url.replace( @@ -469,11 +456,6 @@ export function NeynarAuthButton() { ) ); } else { - setDebugState( - 'Opening access dialog...' + - ` ${clientContext?.platformType}` + - ` ${clientContext?.clientFid}` - ); setDialogStep('access'); setShowDialog(true); } @@ -482,14 +464,11 @@ export function NeynarAuthButton() { startPolling(newSigner.signer_uuid, message, signature); } } else { - setDebugState('Signers already exist, proceeding to signin...'); - // If signers exist, close the dialog setSignersLoading(false); setShowDialog(false); setDialogStep('signin'); } } catch (error) { - setDebugState('Error in signer flow:'); console.error('āŒ Error in signer flow:', error); // On error, reset to signin step setDialogStep('signin'); @@ -578,7 +557,6 @@ export function NeynarAuthButton() { setSignerApprovalUrl(null); setMessage(null); setSignature(null); - setDebugState(null); // Reset polling interval if (pollingInterval) { @@ -657,9 +635,6 @@ export function NeynarAuthButton() { )} -

LocalStorage state

- {window && JSON.stringify(window.localStorage.getItem(STORAGE_KEY))} - {/* Unified Auth Dialog */} { Date: Fri, 11 Jul 2025 03:08:17 +0530 Subject: [PATCH 23/32] skip asking for seedPhrase and should_sponsor --- bin/init.js | 80 +++++++++++++++++------------------------------------ 1 file changed, 26 insertions(+), 54 deletions(-) diff --git a/bin/init.js b/bin/init.js index e878b3d..73a7ed9 100644 --- a/bin/init.js +++ b/bin/init.js @@ -349,34 +349,6 @@ export async function init(projectName = null, autoAcceptDefaults = false) { }, ]); answers.enableAnalytics = analyticsAnswer.enableAnalytics; - - // Ask about SEED_PHRASE - const seedPhraseAnswer = await inquirer.prompt([ - { - type: 'password', - name: 'seedPhrase', - message: - 'āš ļø If SEED_PHRASE is not provided, you will not be able to use Sign In With Neynar.\n\n' + - 'Enter your SEED_PHRASE (or press enter to skip):', - default: null, - }, - ]); - answers.seedPhrase = seedPhraseAnswer.seedPhrase; - - // Ask about sponsor signer if SEED_PHRASE is provided - if (answers.seedPhrase) { - const sponsorSignerAnswer = await inquirer.prompt([ - { - type: 'confirm', - name: 'sponsorSigner', - message: - 'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' + - 'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.', - default: false, - }, - ]); - answers.sponsorSigner = sponsorSignerAnswer.sponsorSigner; - } } const finalProjectName = answers.projectName; @@ -438,32 +410,32 @@ export async function init(projectName = null, autoAcceptDefaults = false) { // Add dependencies packageJson.dependencies = { - "@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-sdk": ">=0.1.6 <1.0.0", - "@farcaster/miniapp-wagmi-connector": "^1.0.0", - "@farcaster/mini-app-solana": ">=0.0.17 <1.0.0", - "@neynar/react": "^1.2.5", - "@radix-ui/react-label": "^2.1.1", - "@solana/wallet-adapter-react": "^0.15.38", - "@tanstack/react-query": "^5.61.0", - "@upstash/redis": "^1.34.3", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "dotenv": "^16.4.7", - "lucide-react": "^0.469.0", - "mipd": "^0.0.7", - "next": "^15", - "next-auth": "^4.24.11", - "react": "^19", - "react-dom": "^19", - "tailwind-merge": "^2.6.0", - "tailwindcss-animate": "^1.0.7", - "viem": "^2.23.6", - "wagmi": "^2.14.12", - "zod": "^3.24.2", - "siwe": '^3.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-sdk': '>=0.1.6 <1.0.0', + '@farcaster/miniapp-wagmi-connector': '^1.0.0', + '@farcaster/mini-app-solana': '>=0.0.17 <1.0.0', + '@neynar/react': '^1.2.5', + '@radix-ui/react-label': '^2.1.1', + '@solana/wallet-adapter-react': '^0.15.38', + '@tanstack/react-query': '^5.61.0', + '@upstash/redis': '^1.34.3', + 'class-variance-authority': '^0.7.1', + clsx: '^2.1.1', + dotenv: '^16.4.7', + 'lucide-react': '^0.469.0', + mipd: '^0.0.7', + next: '^15', + 'next-auth': '^4.24.11', + react: '^19', + 'react-dom': '^19', + 'tailwind-merge': '^2.6.0', + 'tailwindcss-animate': '^1.0.7', + viem: '^2.23.6', + wagmi: '^2.14.12', + zod: '^3.24.2', + siwe: '^3.0.0', }; packageJson.devDependencies = { From 16896db17c5be5c566739fdab0af3fc39ee9c5c6 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 03:19:42 +0530 Subject: [PATCH 24/32] use sdk function --- src/app/api/auth/session-signers/route.ts | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/app/api/auth/session-signers/route.ts b/src/app/api/auth/session-signers/route.ts index 9d41b0d..7a72016 100644 --- a/src/app/api/auth/session-signers/route.ts +++ b/src/app/api/auth/session-signers/route.ts @@ -22,18 +22,13 @@ export async function GET(request: Request) { // Fetch user data if signers exist let user = null; - if (signers && signers.length > 0) { - try { - const userResponse = await fetch( - `${process.env.NEXTAUTH_URL}/api/users?fids=${signers[0].fid}` - ); - if (userResponse.ok) { - const userDataResponse = await userResponse.json(); - user = userDataResponse.users?.[0] || null; - } - } catch (error) { - console.error('Error fetching user data:', error); - } + if (signers && signers.length > 0 && signers[0].fid) { + const { + users: [fetchedUser], + } = await client.fetchBulkUsers({ + fids: [signers[0].fid], + }); + user = fetchedUser; } return NextResponse.json({ From 2d259d22b0281aec75c54148c8315e5d83b29f9c Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 03:25:17 +0530 Subject: [PATCH 25/32] remove unnecessary post request --- src/app/api/auth/session-signers/route.ts | 42 ----------------------- 1 file changed, 42 deletions(-) diff --git a/src/app/api/auth/session-signers/route.ts b/src/app/api/auth/session-signers/route.ts index 7a72016..630ef3b 100644 --- a/src/app/api/auth/session-signers/route.ts +++ b/src/app/api/auth/session-signers/route.ts @@ -1,6 +1,4 @@ import { NextResponse } from 'next/server'; -import { getServerSession } from 'next-auth'; -import { authOptions } from '~/auth'; import { getNeynarClient } from '~/lib/neynar'; export async function GET(request: Request) { @@ -43,43 +41,3 @@ export async function GET(request: Request) { ); } } - -export async function POST(request: Request) { - try { - const session = await getServerSession(authOptions); - - if (!session?.user?.fid) { - return NextResponse.json( - { error: 'No authenticated session found' }, - { status: 401 } - ); - } - - const body = await request.json(); - const { message, signature, signers, user } = body; - - if (!message || !signature || !signers) { - return NextResponse.json( - { error: 'Message, signature, and signers are required' }, - { status: 400 } - ); - } - - // Since we can't directly modify the session token here, - // we'll return the data and let the client trigger a session update - // The client will need to call getSession() to refresh the session - - return NextResponse.json({ - success: true, - message: 'Session data prepared for update', - signers, - user, - }); - } catch (error) { - console.error('Error updating session signers:', error); - return NextResponse.json( - { error: 'Failed to update session' }, - { status: 500 } - ); - } -} From e0ca42169b4f4970a6652d7a3f5646ed8097dad1 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 03:39:27 +0530 Subject: [PATCH 26/32] Move constants --- src/app/api/auth/signer/signed_key/route.ts | 18 +++------- src/lib/constants.ts | 39 +++++++++++++++------ 2 files changed, 32 insertions(+), 25 deletions(-) diff --git a/src/app/api/auth/signer/signed_key/route.ts b/src/app/api/auth/signer/signed_key/route.ts index de4c414..d7a3df8 100644 --- a/src/app/api/auth/signer/signed_key/route.ts +++ b/src/app/api/auth/signer/signed_key/route.ts @@ -1,23 +1,13 @@ import { NextResponse } from 'next/server'; import { getNeynarClient } from '~/lib/neynar'; import { mnemonicToAccount } from 'viem/accounts'; +import { + SIGNED_KEY_REQUEST_TYPE, + SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, +} from '~/lib/constants'; const postRequiredFields = ['signerUuid', 'publicKey']; -const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { - name: 'Farcaster SignedKeyRequestValidator', - version: '1', - chainId: 10, - verifyingContract: - '0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`, -}; - -const SIGNED_KEY_REQUEST_TYPE = [ - { name: 'requestFid', type: 'uint256' }, - { name: 'key', type: 'bytes' }, - { name: 'deadline', type: 'uint256' }, -]; - export async function POST(request: Request) { const body = await request.json(); diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 6c980be..7a7661b 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,10 +1,10 @@ /** * Application constants and configuration values. - * + * * This file contains all the configuration constants used throughout the mini app. * These values are either sourced from environment variables or hardcoded and provide * configuration for the app's appearance, behavior, and integration settings. - * + * * NOTE: This file is automatically updated by the init script. * Manual changes may be overwritten during project initialization. */ @@ -20,19 +20,19 @@ export const APP_URL = process.env.NEXT_PUBLIC_URL!; * The name of the mini app as displayed to users. * Used in titles, headers, and app store listings. */ -export const APP_NAME = 'Starter Kit'; +export const APP_NAME = 'shreyas-testing-mini-app'; /** * A brief description of the mini app's functionality. * Used in app store listings and metadata. */ -export const APP_DESCRIPTION = 'A demo of the Neynar Starter Kit'; +export const APP_DESCRIPTION = 'A Farcaster mini app created with Neynar'; /** * The primary category for the mini app. * Used for app store categorization and discovery. */ -export const APP_PRIMARY_CATEGORY = 'developer-tools'; +export const APP_PRIMARY_CATEGORY = ''; /** * Tags associated with the mini app. @@ -63,30 +63,31 @@ export const APP_SPLASH_URL = `${APP_URL}/splash.png`; * Background color for the splash screen. * Used as fallback when splash image is loading. */ -export const APP_SPLASH_BACKGROUND_COLOR = "#f7f7f7"; +export const APP_SPLASH_BACKGROUND_COLOR = '#f7f7f7'; // --- 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 = 'Launch NSK'; +export const APP_BUTTON_TEXT = 'Launch Mini App'; // --- Integration Configuration --- /** * Webhook URL for receiving events from Neynar. - * + * * If Neynar API key and client ID are configured, uses the official * Neynar webhook endpoint. Otherwise, falls back to a local webhook * endpoint for development and testing. */ -export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID +export const APP_WEBHOOK_URL = + 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`; /** * Flag to enable/disable wallet functionality. - * + * * When true, wallet-related components and features are rendered. * When false, wallet functionality is completely hidden from the UI. * Useful for mini apps that don't require wallet integration. @@ -95,9 +96,25 @@ export const USE_WALLET = true; /** * Flag to enable/disable analytics tracking. - * + * * When true, usage analytics are collected and sent to Neynar. * When false, analytics collection is disabled. * Useful for privacy-conscious users or development environments. */ export const ANALYTICS_ENABLED = true; + +// PLEASE DO NOT UPDATE THIS +export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { + name: 'Farcaster SignedKeyRequestValidator', + version: '1', + chainId: 10, + verifyingContract: + '0x00000000fc700472606ed4fa22623acf62c60553' as `0x${string}`, +}; + +// PLEASE DO NOT UPDATE THIS +export const SIGNED_KEY_REQUEST_TYPE = [ + { name: 'requestFid', type: 'uint256' }, + { name: 'key', type: 'bytes' }, + { name: 'deadline', type: 'uint256' }, +]; From 8ff7080e8462a437d70acc61446d614783fc3408 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 03:44:09 +0530 Subject: [PATCH 27/32] Add more info --- scripts/deploy.js | 444 +++++++++++++++++++++++++--------------------- 1 file changed, 243 insertions(+), 201 deletions(-) diff --git a/scripts/deploy.js b/scripts/deploy.js index 0f75f01..9c4ca4d 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -1,33 +1,33 @@ -import { execSync, spawn } from "child_process"; -import fs from "fs"; -import path from "path"; -import os from "os"; -import { fileURLToPath } from "url"; -import inquirer from "inquirer"; -import dotenv from "dotenv"; -import crypto from "crypto"; -import { Vercel } from "@vercel/sdk"; +import { execSync, spawn } from 'child_process'; +import fs from 'fs'; +import path from 'path'; +import os from 'os'; +import { fileURLToPath } from 'url'; +import inquirer from 'inquirer'; +import dotenv from 'dotenv'; +import crypto from 'crypto'; +import { Vercel } from '@vercel/sdk'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.join(__dirname, ".."); +const projectRoot = path.join(__dirname, '..'); // Load environment variables in specific order -dotenv.config({ path: ".env" }); +dotenv.config({ path: '.env' }); async function generateFarcasterMetadata(domain, webhookUrl) { const trimmedDomain = domain.trim(); - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(","); + const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); return { frame: { - version: "1", + version: '1', name: process.env.NEXT_PUBLIC_MINI_APP_NAME, iconUrl: `https://${trimmedDomain}/icon.png`, homeUrl: `https://${trimmedDomain}`, imageUrl: `https://${trimmedDomain}/api/opengraph-image`, buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, splashImageUrl: `https://${trimmedDomain}/splash.png`, - splashBackgroundColor: "#f7f7f7", + splashBackgroundColor: '#f7f7f7', webhookUrl: webhookUrl?.trim(), description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, @@ -38,36 +38,36 @@ async function generateFarcasterMetadata(domain, webhookUrl) { async function loadEnvLocal() { try { - if (fs.existsSync(".env.local")) { + if (fs.existsSync('.env.local')) { const { loadLocal } = await inquirer.prompt([ { - type: "confirm", - name: "loadLocal", + type: 'confirm', + name: 'loadLocal', message: - "Found .env.local - would you like to load its values in addition to .env values?", + 'Found .env.local - would you like to load its values in addition to .env values?', default: true, }, ]); if (loadLocal) { - console.log("Loading values from .env.local..."); - const localEnv = dotenv.parse(fs.readFileSync(".env.local")); + console.log('Loading values from .env.local...'); + const localEnv = dotenv.parse(fs.readFileSync('.env.local')); const allowedVars = [ - "NEXT_PUBLIC_MINI_APP_NAME", - "NEXT_PUBLIC_MINI_APP_DESCRIPTION", - "NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY", - "NEXT_PUBLIC_MINI_APP_TAGS", - "NEXT_PUBLIC_MINI_APP_BUTTON_TEXT", - "NEXT_PUBLIC_ANALYTICS_ENABLED", - "NEYNAR_API_KEY", - "NEYNAR_CLIENT_ID", - "SPONSOR_SIGNER", + 'NEXT_PUBLIC_MINI_APP_NAME', + 'NEXT_PUBLIC_MINI_APP_DESCRIPTION', + 'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY', + 'NEXT_PUBLIC_MINI_APP_TAGS', + 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', + 'NEXT_PUBLIC_ANALYTICS_ENABLED', + 'NEYNAR_API_KEY', + 'NEYNAR_CLIENT_ID', + 'SPONSOR_SIGNER', ]; - const envContent = fs.existsSync(".env") - ? fs.readFileSync(".env", "utf8") + "\n" - : ""; + const envContent = fs.existsSync('.env') + ? fs.readFileSync('.env', 'utf8') + '\n' + : ''; let newEnvContent = envContent; for (const [key, value] of Object.entries(localEnv)) { @@ -79,35 +79,35 @@ async function loadEnvLocal() { } } - fs.writeFileSync(".env", newEnvContent); - console.log("āœ… Values from .env.local have been written to .env"); + fs.writeFileSync('.env', newEnvContent); + console.log('āœ… Values from .env.local have been written to .env'); } } } catch (error) { - console.log("Note: No .env.local file found"); + console.log('Note: No .env.local file found'); } } async function checkRequiredEnvVars() { - console.log("\nšŸ“ Checking environment variables..."); - console.log("Loading values from .env..."); + console.log('\nšŸ“ Checking environment variables...'); + console.log('Loading values from .env...'); await loadEnvLocal(); const requiredVars = [ { - name: "NEXT_PUBLIC_MINI_APP_NAME", - message: "Enter the name for your frame (e.g., My Cool Mini App):", + name: 'NEXT_PUBLIC_MINI_APP_NAME', + message: 'Enter the name for your frame (e.g., My Cool Mini App):', default: process.env.NEXT_PUBLIC_MINI_APP_NAME, validate: (input) => - input.trim() !== "" || "Mini app name cannot be empty", + input.trim() !== '' || 'Mini app name cannot be empty', }, { - name: "NEXT_PUBLIC_MINI_APP_BUTTON_TEXT", - message: "Enter the text for your frame button:", + name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', + message: 'Enter the text for your frame button:', default: - process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? "Launch Mini App", - validate: (input) => input.trim() !== "" || "Button text cannot be empty", + process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', + validate: (input) => input.trim() !== '' || 'Button text cannot be empty', }, ]; @@ -120,8 +120,8 @@ async function checkRequiredEnvVars() { for (const varConfig of missingVars) { const { value } = await inquirer.prompt([ { - type: "input", - name: "value", + type: 'input', + name: 'value', message: varConfig.message, default: varConfig.default, validate: varConfig.validate, @@ -130,14 +130,14 @@ async function checkRequiredEnvVars() { process.env[varConfig.name] = value; - const envContent = fs.existsSync(".env") - ? fs.readFileSync(".env", "utf8") - : ""; + const envContent = fs.existsSync('.env') + ? fs.readFileSync('.env', 'utf8') + : ''; if (!envContent.includes(`${varConfig.name}=`)) { - const newLine = envContent ? "\n" : ""; + const newLine = envContent ? '\n' : ''; fs.appendFileSync( - ".env", + '.env', `${newLine}${varConfig.name}="${value.trim()}"` ); } @@ -150,15 +150,19 @@ async function checkRequiredEnvVars() { name: 'sponsorSigner', message: 'Do you want to sponsor the signer? (This will be used in Sign In With Neynar)\n' + - 'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.', + 'Note: If you choose to sponsor the signer, Neynar will sponsor it for you and you will be charged in CUs.\n' + + 'For more information, see https://docs.neynar.com/docs/two-ways-to-sponsor-a-farcaster-signer-via-neynar#sponsor-signers', default: false, }, ]); - + process.env.SPONSOR_SIGNER = sponsorSigner.toString(); - + if (storeSeedPhrase) { - fs.appendFileSync('.env.local', `\nSPONSOR_SIGNER="${sponsorSigner}"`); + fs.appendFileSync( + '.env.local', + `\nSPONSOR_SIGNER="${sponsorSigner}"` + ); console.log('āœ… Sponsor signer preference stored in .env.local'); } } @@ -166,7 +170,11 @@ async function checkRequiredEnvVars() { } // Load SPONSOR_SIGNER from .env.local if SEED_PHRASE exists but SPONSOR_SIGNER doesn't - if (process.env.SEED_PHRASE && !process.env.SPONSOR_SIGNER && fs.existsSync('.env.local')) { + if ( + process.env.SEED_PHRASE && + !process.env.SPONSOR_SIGNER && + fs.existsSync('.env.local') + ) { const localEnv = dotenv.parse(fs.readFileSync('.env.local')); if (localEnv.SPONSOR_SIGNER) { process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; @@ -176,9 +184,9 @@ async function checkRequiredEnvVars() { async function getGitRemote() { try { - const remoteUrl = execSync("git remote get-url origin", { + const remoteUrl = execSync('git remote get-url origin', { cwd: projectRoot, - encoding: "utf8", + encoding: 'utf8', }).trim(); return remoteUrl; } catch (error) { @@ -188,9 +196,9 @@ async function getGitRemote() { async function checkVercelCLI() { try { - execSync("vercel --version", { - stdio: "ignore", - shell: process.platform === "win32", + execSync('vercel --version', { + stdio: 'ignore', + shell: process.platform === 'win32', }); return true; } catch (error) { @@ -199,23 +207,23 @@ async function checkVercelCLI() { } async function installVercelCLI() { - console.log("Installing Vercel CLI..."); - execSync("npm install -g vercel", { - stdio: "inherit", - shell: process.platform === "win32", + console.log('Installing Vercel CLI...'); + execSync('npm install -g vercel', { + stdio: 'inherit', + shell: process.platform === 'win32', }); } async function getVercelToken() { try { // Try to get token from Vercel CLI config - const configPath = path.join(os.homedir(), ".vercel", "auth.json"); + const configPath = path.join(os.homedir(), '.vercel', 'auth.json'); if (fs.existsSync(configPath)) { - const authConfig = JSON.parse(fs.readFileSync(configPath, "utf8")); + const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); return authConfig.token; } } catch (error) { - console.warn("Could not read Vercel token from config file"); + console.warn('Could not read Vercel token from config file'); } // Try environment variable @@ -225,75 +233,75 @@ async function getVercelToken() { // Try to extract from vercel whoami try { - const whoamiOutput = execSync("vercel whoami", { - encoding: "utf8", - stdio: "pipe", + const whoamiOutput = execSync('vercel whoami', { + encoding: 'utf8', + stdio: 'pipe', }); // If we can get whoami, we're logged in, but we need the actual token // The token isn't directly exposed, so we'll need to use CLI for some operations - console.log("āœ… Verified Vercel CLI authentication"); + console.log('āœ… Verified Vercel CLI authentication'); return null; // We'll fall back to CLI operations } catch (error) { throw new Error( - "Not logged in to Vercel CLI. Please run this script again to login." + 'Not logged in to Vercel CLI. Please run this script again to login.' ); } } async function loginToVercel() { - console.log("\nšŸ”‘ Vercel Login"); - console.log("You can either:"); - console.log("1. Log in to an existing Vercel account"); - console.log("2. Create a new Vercel account during login\n"); - console.log("If creating a new account:"); + console.log('\nšŸ”‘ Vercel Login'); + console.log('You can either:'); + console.log('1. Log in to an existing Vercel account'); + console.log('2. Create a new Vercel account during login\n'); + console.log('If creating a new account:'); console.log('1. Click "Continue with GitHub"'); - console.log("2. Authorize GitHub access"); - console.log("3. Complete the Vercel account setup in your browser"); - console.log("4. Return here once your Vercel account is created\n"); + console.log('2. Authorize GitHub access'); + console.log('3. Complete the Vercel account setup in your browser'); + console.log('4. Return here once your Vercel account is created\n'); console.log( - "\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account" + '\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account' ); - const child = spawn("vercel", ["login"], { - stdio: "inherit", + const child = spawn('vercel', ['login'], { + stdio: 'inherit', }); await new Promise((resolve, reject) => { - child.on("close", (code) => { + child.on('close', (code) => { resolve(); }); }); - console.log("\nšŸ“± Waiting for login to complete..."); + console.log('\nšŸ“± Waiting for login to complete...'); console.log( "If you're creating a new account, please complete the Vercel account setup in your browser first." ); for (let i = 0; i < 150; i++) { try { - execSync("vercel whoami", { stdio: "ignore" }); - console.log("āœ… Successfully logged in to Vercel!"); + execSync('vercel whoami', { stdio: 'ignore' }); + console.log('āœ… Successfully logged in to Vercel!'); return true; } catch (error) { - if (error.message.includes("Account not found")) { - console.log("ā„¹ļø Waiting for Vercel account setup to complete..."); + if (error.message.includes('Account not found')) { + console.log('ā„¹ļø Waiting for Vercel account setup to complete...'); } await new Promise((resolve) => setTimeout(resolve, 2000)); } } - console.error("\nāŒ Login timed out. Please ensure you have:"); - console.error("1. Completed the Vercel account setup in your browser"); - console.error("2. Authorized the GitHub integration"); - console.error("Then try running this script again."); + console.error('\nāŒ Login timed out. Please ensure you have:'); + console.error('1. Completed the Vercel account setup in your browser'); + console.error('2. Authorized the GitHub integration'); + console.error('Then try running this script again.'); return false; } async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { try { let processedValue; - if (typeof value === "object") { + if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { processedValue = value.toString(); @@ -305,7 +313,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { }); const existingVar = existingVars.envs?.find( - (env) => env.key === key && env.target?.includes("production") + (env) => env.key === key && env.target?.includes('production') ); if (existingVar) { @@ -315,7 +323,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { id: existingVar.id, requestBody: { value: processedValue, - target: ["production"], + target: ['production'], }, }); console.log(`āœ… Updated environment variable: ${key}`); @@ -326,8 +334,8 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { requestBody: { key: key, value: processedValue, - type: "encrypted", - target: ["production"], + type: 'encrypted', + target: ['production'], }, }); console.log(`āœ… Created environment variable: ${key}`); @@ -349,7 +357,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { try { execSync(`vercel env rm ${key} production -y`, { cwd: projectRoot, - stdio: "ignore", + stdio: 'ignore', env: process.env, }); } catch (error) { @@ -357,7 +365,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { } let processedValue; - if (typeof value === "object") { + if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { processedValue = value.toString(); @@ -365,11 +373,11 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { // Create temporary file const tempFilePath = path.join(projectRoot, `${key}_temp.txt`); - fs.writeFileSync(tempFilePath, processedValue, "utf8"); + fs.writeFileSync(tempFilePath, processedValue, 'utf8'); // Use appropriate command based on platform let command; - if (process.platform === "win32") { + if (process.platform === 'win32') { command = `type "${tempFilePath}" | vercel env add ${key} production`; } else { command = `cat "${tempFilePath}" | vercel env add ${key} production`; @@ -377,7 +385,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { execSync(command, { cwd: projectRoot, - stdio: "pipe", // Changed from 'inherit' to avoid interactive prompts + stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts shell: true, env: process.env, }); @@ -404,7 +412,7 @@ async function setEnvironmentVariables( envVars, projectRoot ) { - console.log("\nšŸ“ Setting up environment variables..."); + console.log('\nšŸ“ Setting up environment variables...'); const results = []; @@ -432,28 +440,33 @@ async function setEnvironmentVariables( console.warn(`\nāš ļø Failed to set ${failed.length} environment variables:`); failed.forEach((r) => console.warn(` - ${r.key}`)); console.warn( - "\nYou may need to set these manually in the Vercel dashboard." + '\nYou may need to set these manually in the Vercel dashboard.' ); } return results; } -async function waitForDeployment(vercelClient, projectId, maxWaitTime = 300000) { // 5 minutes +async function waitForDeployment( + vercelClient, + projectId, + maxWaitTime = 300000 +) { + // 5 minutes console.log('\nā³ Waiting for deployment to complete...'); const startTime = Date.now(); - + while (Date.now() - startTime < maxWaitTime) { try { const deployments = await vercelClient.deployments.list({ projectId: projectId, - limit: 1 + limit: 1, }); - + if (deployments.deployments?.[0]) { const deployment = deployments.deployments[0]; console.log(`šŸ“Š Deployment status: ${deployment.state}`); - + if (deployment.state === 'READY') { console.log('āœ… Deployment completed successfully!'); return deployment; @@ -462,36 +475,36 @@ async function waitForDeployment(vercelClient, projectId, maxWaitTime = 300000) } else if (deployment.state === 'CANCELED') { throw new Error('Deployment was canceled'); } - + // Still building, wait and check again - await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds + await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds } else { console.log('ā³ No deployment found yet, waiting...'); - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); } } catch (error) { console.warn('āš ļø Could not check deployment status:', error.message); - await new Promise(resolve => setTimeout(resolve, 5000)); + await new Promise((resolve) => setTimeout(resolve, 5000)); } } - + throw new Error('Deployment timed out after 5 minutes'); } async function deployToVercel(useGitHub = false) { try { - console.log("\nšŸš€ Deploying to Vercel..."); + console.log('\nšŸš€ Deploying to Vercel...'); // Ensure vercel.json exists - const vercelConfigPath = path.join(projectRoot, "vercel.json"); + const vercelConfigPath = path.join(projectRoot, 'vercel.json'); if (!fs.existsSync(vercelConfigPath)) { - console.log("šŸ“ Creating vercel.json configuration..."); + console.log('šŸ“ Creating vercel.json configuration...'); fs.writeFileSync( vercelConfigPath, JSON.stringify( { - buildCommand: "next build", - framework: "nextjs", + buildCommand: 'next build', + framework: 'nextjs', }, null, 2 @@ -501,15 +514,19 @@ async function deployToVercel(useGitHub = false) { // Set up Vercel project console.log('\nšŸ“¦ Setting up Vercel project...'); - console.log('An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n'); - console.log('\nāš ļø Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n'); - + console.log( + 'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n' + ); + console.log( + '\nāš ļø Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n' + ); + // Use spawn instead of execSync for better error handling const { spawn } = await import('child_process'); - const vercelSetup = spawn('vercel', [], { + const vercelSetup = spawn('vercel', [], { cwd: projectRoot, - stdio: "inherit", - shell: process.platform === "win32", + stdio: 'inherit', + shell: process.platform === 'win32', }); await new Promise((resolve, reject) => { @@ -522,7 +539,7 @@ async function deployToVercel(useGitHub = false) { resolve(); // Don't reject, as this is often expected } }); - + vercelSetup.on('error', (error) => { console.log('āš ļø Vercel setup command completed (this is normal)'); resolve(); // Don't reject, as this is often expected @@ -530,15 +547,19 @@ async function deployToVercel(useGitHub = false) { }); // Wait a moment for project files to be written - await new Promise(resolve => setTimeout(resolve, 2000)); + await new Promise((resolve) => setTimeout(resolve, 2000)); // Load project info let projectId; try { - const projectJson = JSON.parse(fs.readFileSync('.vercel/project.json', 'utf8')); + const projectJson = JSON.parse( + fs.readFileSync('.vercel/project.json', 'utf8') + ); projectId = projectJson.projectId; } catch (error) { - throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.'); + throw new Error( + 'Failed to load project info. Please ensure the Vercel project was created successfully.' + ); } // Get Vercel token and initialize SDK client @@ -549,16 +570,16 @@ async function deployToVercel(useGitHub = false) { vercelClient = new Vercel({ bearerToken: token, }); - console.log("āœ… Initialized Vercel SDK client"); + console.log('āœ… Initialized Vercel SDK client'); } } catch (error) { console.warn( - "āš ļø Could not initialize Vercel SDK, falling back to CLI operations" + 'āš ļø Could not initialize Vercel SDK, falling back to CLI operations' ); } // Get project details - console.log("\nšŸ” Getting project details..."); + console.log('\nšŸ” Getting project details...'); let domain; let projectName; @@ -569,10 +590,10 @@ async function deployToVercel(useGitHub = false) { }); projectName = project.name; domain = `${projectName}.vercel.app`; - console.log("🌐 Using project name for domain:", domain); + console.log('🌐 Using project name for domain:', domain); } catch (error) { console.warn( - "āš ļø Could not get project details via SDK, using CLI fallback" + 'āš ļø Could not get project details via SDK, using CLI fallback' ); } } @@ -580,16 +601,19 @@ async function deployToVercel(useGitHub = false) { // Fallback to CLI method if SDK failed if (!domain) { try { - const inspectOutput = execSync(`vercel project inspect ${projectId} 2>&1`, { - cwd: projectRoot, - encoding: 'utf8' - }); + const inspectOutput = execSync( + `vercel project inspect ${projectId} 2>&1`, + { + cwd: projectRoot, + encoding: 'utf8', + } + ); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); if (nameMatch) { projectName = nameMatch[1].trim(); domain = `${projectName}.vercel.app`; - console.log("🌐 Using project name for domain:", domain); + console.log('🌐 Using project name for domain:', domain); } else { const altMatch = inspectOutput.match(/Found Project [^/]+\/([^\n]+)/); if (altMatch) { @@ -597,7 +621,9 @@ async function deployToVercel(useGitHub = false) { domain = `${projectName}.vercel.app`; console.log('🌐 Using project name for domain:', domain); } else { - console.warn('āš ļø Could not determine project name from inspection, using fallback'); + console.warn( + 'āš ļø Could not determine project name from inspection, using fallback' + ); // Use a fallback domain based on project ID domain = `project-${projectId.slice(-8)}.vercel.app`; console.log('🌐 Using fallback domain:', domain); @@ -612,7 +638,7 @@ async function deployToVercel(useGitHub = false) { } // Generate mini app metadata - console.log("\nšŸ”Ø Generating mini app metadata..."); + console.log('\nšŸ”Ø Generating mini app metadata...'); const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID @@ -620,11 +646,11 @@ async function deployToVercel(useGitHub = false) { : `https://${domain}/api/webhook`; const miniAppMetadata = await generateFarcasterMetadata(domain, webhookUrl); - console.log("āœ… Mini app metadata generated"); + console.log('āœ… Mini app metadata generated'); // Prepare environment variables const nextAuthSecret = - process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex"); + process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex'); const vercelEnv = { NEXTAUTH_SECRET: nextAuthSecret, AUTH_SECRET: nextAuthSecret, @@ -637,12 +663,14 @@ async function deployToVercel(useGitHub = false) { ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID, }), - ...(process.env.SPONSOR_SIGNER && { SPONSOR_SIGNER: process.env.SPONSOR_SIGNER }), + ...(process.env.SPONSOR_SIGNER && { + SPONSOR_SIGNER: process.env.SPONSOR_SIGNER, + }), ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }), ...Object.fromEntries( Object.entries(process.env).filter(([key]) => - key.startsWith("NEXT_PUBLIC_") + key.startsWith('NEXT_PUBLIC_') ) ), }; @@ -657,21 +685,21 @@ async function deployToVercel(useGitHub = false) { // Deploy the project if (useGitHub) { - console.log("\nSetting up GitHub integration..."); - execSync("vercel link", { + console.log('\nSetting up GitHub integration...'); + execSync('vercel link', { cwd: projectRoot, - stdio: "inherit", + stdio: 'inherit', env: process.env, }); - console.log("\nšŸ“¦ Deploying with GitHub integration..."); + console.log('\nšŸ“¦ Deploying with GitHub integration...'); } else { - console.log("\nšŸ“¦ Deploying local code directly..."); + console.log('\nšŸ“¦ Deploying local code directly...'); } // Use spawn for better control over the deployment process - const vercelDeploy = spawn('vercel', ['deploy', '--prod'], { + const vercelDeploy = spawn('vercel', ['deploy', '--prod'], { cwd: projectRoot, - stdio: "inherit", + stdio: 'inherit', env: process.env, }); @@ -685,7 +713,7 @@ async function deployToVercel(useGitHub = false) { reject(new Error(`Vercel deployment failed with exit code: ${code}`)); } }); - + vercelDeploy.on('error', (error) => { console.error('āŒ Vercel deployment error:', error.message); reject(error); @@ -698,13 +726,16 @@ async function deployToVercel(useGitHub = false) { try { deployment = await waitForDeployment(vercelClient, projectId); } catch (error) { - console.warn('āš ļø Could not verify deployment completion:', error.message); + console.warn( + 'āš ļø Could not verify deployment completion:', + error.message + ); console.log('ā„¹ļø Proceeding with domain verification...'); } } // Verify actual domain after deployment - console.log("\nšŸ” Verifying deployment domain..."); + console.log('\nšŸ” Verifying deployment domain...'); let actualDomain = domain; if (vercelClient && deployment) { @@ -713,14 +744,14 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Verified actual domain:', actualDomain); } catch (error) { console.warn( - "āš ļø Could not verify domain via SDK, using assumed domain" + 'āš ļø Could not verify domain via SDK, using assumed domain' ); } } // Update environment variables if domain changed if (actualDomain !== domain) { - console.log("šŸ”„ Updating environment variables with correct domain..."); + console.log('šŸ”„ Updating environment variables with correct domain...'); const webhookUrl = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID @@ -733,16 +764,27 @@ async function deployToVercel(useGitHub = false) { }; if (miniAppMetadata) { - const updatedMetadata = await generateFarcasterMetadata(actualDomain, fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); + const updatedMetadata = await generateFarcasterMetadata( + actualDomain, + fid, + await validateSeedPhrase(process.env.SEED_PHRASE), + process.env.SEED_PHRASE, + webhookUrl + ); updatedEnv.MINI_APP_METADATA = updatedMetadata; } - await setEnvironmentVariables(vercelClient, projectId, updatedEnv, projectRoot); + await setEnvironmentVariables( + vercelClient, + projectId, + updatedEnv, + projectRoot + ); console.log('\nšŸ“¦ Redeploying with correct domain...'); - const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], { + const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], { cwd: projectRoot, - stdio: "inherit", + stdio: 'inherit', env: process.env, }); @@ -756,49 +798,49 @@ async function deployToVercel(useGitHub = false) { reject(new Error(`Redeployment failed with exit code: ${code}`)); } }); - + vercelRedeploy.on('error', (error) => { console.error('āŒ Redeployment error:', error.message); reject(error); }); }); - + domain = actualDomain; } - console.log("\n✨ Deployment complete! Your mini app is now live at:"); + console.log('\n✨ Deployment complete! Your mini app is now live at:'); console.log(`🌐 https://${domain}`); console.log( - "\nšŸ“ You can manage your project at https://vercel.com/dashboard" + '\nšŸ“ You can manage your project at https://vercel.com/dashboard' ); } catch (error) { - console.error("\nāŒ Deployment failed:", error.message); + console.error('\nāŒ Deployment failed:', error.message); process.exit(1); } } async function main() { try { - console.log("šŸš€ Vercel Mini App Deployment (SDK Edition)"); + console.log('šŸš€ Vercel Mini App Deployment (SDK Edition)'); console.log( - "This script will deploy your mini app to Vercel using the Vercel SDK." + 'This script will deploy your mini app to Vercel using the Vercel SDK.' ); - console.log("\nThe script will:"); - console.log("1. Check for required environment variables"); - console.log("2. Set up a Vercel project (new or existing)"); - console.log("3. Configure environment variables in Vercel using SDK"); - console.log("4. Deploy and build your mini app\n"); + console.log('\nThe script will:'); + console.log('1. Check for required environment variables'); + console.log('2. Set up a Vercel project (new or existing)'); + console.log('3. Configure environment variables in Vercel using SDK'); + console.log('4. Deploy and build your mini app\n'); // Check if @vercel/sdk is installed try { - await import("@vercel/sdk"); + await import('@vercel/sdk'); } catch (error) { - console.log("šŸ“¦ Installing @vercel/sdk..."); - execSync("npm install @vercel/sdk", { + console.log('šŸ“¦ Installing @vercel/sdk...'); + execSync('npm install @vercel/sdk', { cwd: projectRoot, - stdio: "inherit", + stdio: 'inherit', }); - console.log("āœ… @vercel/sdk installed successfully"); + console.log('āœ… @vercel/sdk installed successfully'); } await checkRequiredEnvVars(); @@ -807,55 +849,55 @@ async function main() { let useGitHub = false; if (remoteUrl) { - console.log("\nšŸ“¦ Found GitHub repository:", remoteUrl); + console.log('\nšŸ“¦ Found GitHub repository:', remoteUrl); const { useGitHubDeploy } = await inquirer.prompt([ { - type: "confirm", - name: "useGitHubDeploy", - message: "Would you like to deploy from the GitHub repository?", + type: 'confirm', + name: 'useGitHubDeploy', + message: 'Would you like to deploy from the GitHub repository?', default: true, }, ]); useGitHub = useGitHubDeploy; } else { - console.log("\nāš ļø No GitHub repository found."); + console.log('\nāš ļø No GitHub repository found.'); const { action } = await inquirer.prompt([ { - type: "list", - name: "action", - message: "What would you like to do?", + type: 'list', + name: 'action', + message: 'What would you like to do?', choices: [ - { name: "Deploy local code directly", value: "deploy" }, - { name: "Set up GitHub repository first", value: "setup" }, + { name: 'Deploy local code directly', value: 'deploy' }, + { name: 'Set up GitHub repository first', value: 'setup' }, ], - default: "deploy", + default: 'deploy', }, ]); - if (action === "setup") { - console.log("\nšŸ‘‹ Please set up your GitHub repository first:"); - console.log("1. Create a new repository on GitHub"); - console.log("2. Run these commands:"); - console.log(" git remote add origin "); - console.log(" git push -u origin main"); - console.log("\nThen run this script again to deploy."); + if (action === 'setup') { + console.log('\nšŸ‘‹ Please set up your GitHub repository first:'); + console.log('1. Create a new repository on GitHub'); + console.log('2. Run these commands:'); + console.log(' git remote add origin '); + console.log(' git push -u origin main'); + console.log('\nThen run this script again to deploy.'); process.exit(0); } } if (!(await checkVercelCLI())) { - console.log("Vercel CLI not found. Installing..."); + console.log('Vercel CLI not found. Installing...'); await installVercelCLI(); } if (!(await loginToVercel())) { - console.error("\nāŒ Failed to log in to Vercel. Please try again."); + console.error('\nāŒ Failed to log in to Vercel. Please try again.'); process.exit(1); } await deployToVercel(useGitHub); } catch (error) { - console.error("\nāŒ Error:", error.message); + console.error('\nāŒ Error:', error.message); process.exit(1); } } From c7583b2ffee66d146e4a010156ebddf3a4a01f06 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 19:35:09 +0530 Subject: [PATCH 28/32] Fix modal not showing --- src/components/ui/NeynarAuthButton/index.tsx | 58 ++++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index c8f1492..0391818 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -423,9 +423,21 @@ export function NeynarAuthButton() { if (message && signature) { const handleSignerFlow = async () => { try { + const clientContext = context?.client as Record; + const isMobileContext = + clientContext?.platformType === 'mobile' && + clientContext?.clientFid === FARCASTER_FID; + + // Step 1: Change to loading state + setDialogStep('loading'); + + // Show dialog if not using backend flow or in browser farcaster + if ((useBackendFlow && !isMobileContext) || !useBackendFlow) + setShowDialog(true); + // First, fetch existing signers const signers = await fetchAllSigners(message, signature); - setSignersLoading(true); + if (useBackendFlow && isMobileContext) setSignersLoading(true); // Check if no signers exist or if we have empty signers if (!signers || signers.length === 0) { @@ -439,40 +451,36 @@ export function NeynarAuthButton() { ); // Step 3: Show QR code in access dialog for signer approval - if (signedKeyData.signer_approval_url) { - setSignerApprovalUrl(signedKeyData.signer_approval_url); - // Check if we're in a mobile context - const clientContext = context?.client as Record; - const isMobileContext = - clientContext?.platformType === 'mobile' && - clientContext?.clientFid === FARCASTER_FID; + setSignerApprovalUrl(signedKeyData.signer_approval_url); - if (isMobileContext) { - setShowDialog(false); - await sdk.actions.openUrl( - signedKeyData.signer_approval_url.replace( - 'https://client.farcaster.xyz/deeplinks/', - 'farcaster://' - ) - ); - } else { - setDialogStep('access'); - setShowDialog(true); - } - - // Step 4: Start polling for signer approval - startPolling(newSigner.signer_uuid, message, signature); + if (isMobileContext) { + setShowDialog(false); + await sdk.actions.openUrl( + signedKeyData.signer_approval_url.replace( + 'https://client.farcaster.xyz/deeplinks/', + 'farcaster://' + ) + ); + } else { + setShowDialog(true); // Ensure dialog is shown during loading + setDialogStep('access'); } + + // Step 4: Start polling for signer approval + startPolling(newSigner.signer_uuid, message, signature); } else { + // If signers exist, close the dialog setSignersLoading(false); setShowDialog(false); setDialogStep('signin'); } } catch (error) { console.error('āŒ Error in signer flow:', error); - // On error, reset to signin step + // On error, reset to signin step and hide dialog setDialogStep('signin'); setSignersLoading(false); + setShowDialog(false); + setSignerApprovalUrl(null); } }; @@ -485,6 +493,8 @@ export function NeynarAuthButton() { createSigner, generateSignedKeyRequest, startPolling, + context, + useBackendFlow, ]); // Backend flow using NextAuth From 5724c92b885b12b857c222d39c948eb36051e90e Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Fri, 11 Jul 2025 20:51:12 +0530 Subject: [PATCH 29/32] Fix:auto redirect --- .../ui/NeynarAuthButton/AuthDialog.tsx | 136 +++++++++--------- src/components/ui/NeynarAuthButton/index.tsx | 9 +- 2 files changed, 71 insertions(+), 74 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 69c2c9a..724d018 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -118,9 +118,9 @@ export function AuthDialog({ const content = getStepContent(); return ( -
-
-
+
+
+

{isError ? 'Error' : content.title}

@@ -144,73 +144,75 @@ export function AuthDialog({
- {isError ? ( -
-
- {error?.message || 'Unknown error, please try again.'} +
+ {isError ? ( +
+
+ {error?.message || 'Unknown error, please try again.'} +
-
- ) : ( -
-
- {typeof content.description === 'string' ? ( -

- {content.description} -

- ) : ( - content.description + ) : ( +
+
+ {typeof content.description === 'string' ? ( +

+ {content.description} +

+ ) : ( + content.description + )} +
+ +
+ {content.showQR && content.qrUrl ? ( +
+ {/* eslint-disable-next-line @next/next/no-img-element */} + QR Code +
+ ) : step === 'loading' || isLoading ? ( +
+
+
+ + {step === 'loading' + ? 'Setting up access...' + : 'Loading...'} + +
+
+ ) : null} +
+ + {content.showOpenButton && content.qrUrl && ( + )}
- -
- {content.showQR && content.qrUrl ? ( -
- {/* eslint-disable-next-line @next/next/no-img-element */} - QR Code -
- ) : step === 'loading' || isLoading ? ( -
-
-
- - {step === 'loading' - ? 'Setting up access...' - : 'Loading...'} - -
-
- ) : null} -
- - {content.showOpenButton && content.qrUrl && ( - - )} -
- )} + )} +
); diff --git a/src/components/ui/NeynarAuthButton/index.tsx b/src/components/ui/NeynarAuthButton/index.tsx index 0391818..c78516e 100644 --- a/src/components/ui/NeynarAuthButton/index.tsx +++ b/src/components/ui/NeynarAuthButton/index.tsx @@ -378,7 +378,7 @@ export function NeynarAuthButton() { } // For backend flow, the session will be handled by NextAuth }, - [useBackendFlow] + [useBackendFlow, fetchUserData] ); // Error callback @@ -538,12 +538,7 @@ export function NeynarAuthButton() { setDialogStep('signin'); setShowDialog(true); frontendSignIn(); - - // Open mobile app if on mobile and URL is available - if (url && isMobile()) { - window.open(url, '_blank'); - } - }, [isError, reconnect, frontendSignIn, url]); + }, [isError, reconnect, frontendSignIn]); const handleSignOut = useCallback(async () => { try { From 73617a18f29171da31df699bbb4a7fbb816cdb87 Mon Sep 17 00:00:00 2001 From: Manan Date: Fri, 11 Jul 2025 16:26:20 -0700 Subject: [PATCH 30/32] Update src/components/ui/NeynarAuthButton/AuthDialog.tsx Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/components/ui/NeynarAuthButton/AuthDialog.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 724d018..436efb0 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -25,9 +25,9 @@ export function AuthDialog({ switch (step) { case 'signin': return { - title: 'Signin', + title: 'Sign in', description: - "To signin, scan the code below with your phone's camera.", + "To sign in, scan the code below with your phone's camera.", showQR: true, qrUrl: url, showOpenButton: true, From 5fa624a0633a87182051d4abde882d140aa60975 Mon Sep 17 00:00:00 2001 From: veganbeef Date: Fri, 11 Jul 2025 22:28:05 -0700 Subject: [PATCH 31/32] fix: auth kit url update and seed phrase input --- bin/init.js | 38 ++++++++- package.json | 2 +- .../ui/NeynarAuthButton/AuthDialog.tsx | 30 +++---- src/components/ui/NeynarAuthButton/index.tsx | 79 ++++++++++++++----- 4 files changed, 113 insertions(+), 36 deletions(-) diff --git a/bin/init.js b/bin/init.js index 73a7ed9..47eb604 100644 --- a/bin/init.js +++ b/bin/init.js @@ -338,6 +338,43 @@ export async function init(projectName = null, autoAcceptDefaults = false) { ]); answers.useTunnel = hostingAnswer.useTunnel; + // Ask about Neynar Sponsored Signers / SIWN + const sponsoredSignerAnswer = await inquirer.prompt([ + { + type: 'confirm', + name: 'useSponsoredSigner', + message: + 'Would you like to use Neynar Sponsored Signers and/or Sign In With Neynar (SIWN)?\n' + + 'This enables the simplest, most secure, and most user-friendly Farcaster authentication for your app.\n\n' + + 'Benefits of using Neynar Sponsored Signers/SIWN:\n' + + '- No auth buildout or signer management required for developers\n' + + '- Cost-effective for users (no gas for signers)\n' + + '- Users can revoke signers at any time\n' + + '- Plug-and-play for web and React Native\n' + + '- Recommended for most developers\n' + + '\nāš ļø A seed phrase is required for this option.\n', + default: false, + }, + ]); + answers.useSponsoredSigner = sponsoredSignerAnswer.useSponsoredSigner; + + if (answers.useSponsoredSigner) { + const { seedPhrase } = await inquirer.prompt([ + { + type: 'password', + name: 'seedPhrase', + message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):', + validate: (input) => { + if (!input || input.trim().split(' ').length < 12) { + return 'Seed phrase must be at least 12 words'; + } + return true; + }, + }, + ]); + answers.seedPhrase = seedPhrase; + } + // Ask about analytics opt-out const analyticsAnswer = await inquirer.prompt([ { @@ -601,7 +638,6 @@ export async function init(projectName = null, autoAcceptDefaults = false) { } if (answers.seedPhrase) { fs.appendFileSync(envPath, `\nSEED_PHRASE="${answers.seedPhrase}"`); - fs.appendFileSync(envPath, `\nSPONSOR_SIGNER="${answers.sponsorSigner}"`); } fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`); diff --git a/package.json b/package.json index 775601a..e893232 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.7", + "version": "1.5.8", "type": "module", "private": false, "access": "public", diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 436efb0..a458ab4 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -191,20 +191,22 @@ export function AuthDialog({ {content.showOpenButton && content.qrUrl && (