diff --git a/.env.example b/.env.example index db836f3..4711c0e 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,7 @@ -KV_REST_API_TOKEN= -KV_REST_API_URL= -NEXT_PUBLIC_URL= -NEXTAUTH_URL= -NEYNAR_API_KEY=FARCASTER_V2_FRAMES_DEMO +# Default values for local development +# For production, update these URLs to your deployed domain +KV_REST_API_TOKEN='' +KV_REST_API_URL='' +NEXT_PUBLIC_URL='http://localhost:3000' +NEXTAUTH_URL='http://localhost:3000' +NEYNAR_API_KEY='FARCASTER_V2_FRAMES_DEMO' diff --git a/bin/index.js b/bin/index.js index af66185..bbd7701 100755 --- a/bin/index.js +++ b/bin/index.js @@ -12,7 +12,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const REPO_URL = 'https://github.com/lucas-neynar/frames-v2-quickstart.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; async function init() { const answers = await inquirer.prompt([ @@ -38,21 +38,10 @@ async function init() { return true; } }, - { - type: 'input', - name: 'fid', - message: 'Enter your Farcaster FID:', - validate: (input) => { - if (input.trim() === '') { - return 'FID cannot be empty'; - } - return true; - } - }, { type: 'password', name: 'seedPhrase', - message: 'Enter your Farcaster account seed phrase:', + message: 'Enter your Farcaster custody account seed phrase:', validate: (input) => { if (input.trim() === '') { return 'Seed phrase cannot be empty'; @@ -68,20 +57,18 @@ async function init() { console.log(`\nCreating a new Frames v2 app in ${projectPath}\n`); // Clone the repository - execSync(`git clone ${REPO_URL} "${projectPath}"`); + try { + execSync(`git clone ${REPO_URL} "${projectPath}"`); + } catch (error) { + console.error('\n❌ Error: Failed to create project directory.'); + console.error('Please make sure you have write permissions and try again.'); + process.exit(1); + } // Remove the .git directory console.log('\nRemoving .git directory...'); fs.rmSync(path.join(projectPath, '.git'), { recursive: true, force: true }); - // Generate manifest and write to public folder - console.log('\nGenerating manifest...'); - const manifest = await generateManifest(answers.fid, answers.seedPhrase); - fs.writeFileSync( - path.join(projectPath, 'public/manifest.json'), - JSON.stringify(manifest) - ); - // Update package.json console.log('\nUpdating package.json...'); const packageJsonPath = path.join(projectPath, 'package.json'); @@ -96,28 +83,32 @@ async function init() { delete packageJson.files; fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2)); - // Remove the bin directory - console.log('\nRemoving bin directory...'); - const binPath = path.join(projectPath, 'bin'); - if (fs.existsSync(binPath)) { - fs.rmSync(binPath, { recursive: true, force: true }); - } - // Handle .env file console.log('\nSetting up environment variables...'); const envExamplePath = path.join(projectPath, '.env.example'); const envPath = path.join(projectPath, '.env'); if (fs.existsSync(envExamplePath)) { - fs.copyFileSync(envExamplePath, envPath); - fs.unlinkSync(envExamplePath); + // Read the example file content + const envExampleContent = fs.readFileSync(envExamplePath, 'utf8'); + // Write it to .env + fs.writeFileSync(envPath, envExampleContent); // Append project name and description to .env fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`); + fs.unlinkSync(envExamplePath); console.log('\nCreated .env file from .env.example'); } else { console.log('\n.env.example does not exist, skipping copy and remove operations'); } + // Generate manifest and write to public folder + console.log('\nGenerating manifest...'); + const manifest = await generateManifest(answers.seedPhrase, projectPath); + fs.writeFileSync( + path.join(projectPath, 'public/manifest.json'), + JSON.stringify(manifest) + ); + // Update README console.log('\nUpdating README...'); const readmePath = path.join(projectPath, 'README.md'); @@ -134,13 +125,20 @@ async function init() { console.log('\nInstalling dependencies...'); execSync('npm install', { cwd: projectPath, stdio: 'inherit' }); + // Remove the bin directory + console.log('\nRemoving bin directory...'); + const binPath = path.join(projectPath, 'bin'); + if (fs.existsSync(binPath)) { + fs.rmSync(binPath, { recursive: true, force: true }); + } + // Initialize git repository console.log('\nInitializing git repository...'); execSync('git init', { cwd: projectPath }); execSync('git add .', { cwd: projectPath }); execSync('git commit -m "initial commit from frames-v2-quickstart"', { cwd: projectPath }); - console.log(`\n🖼️✨ Successfully created frame ${projectName} with git and dependencies installed! ✨🖼️`); + console.log(`\n🪐 ✨ Successfully created frame ${projectName} with git and dependencies installed! ✨🪐`); console.log('\nTo run the app:'); console.log(` cd ${projectName}`); console.log(' npm run dev\n'); diff --git a/bin/manifest.js b/bin/manifest.js index 4d44b86..abef9d6 100644 --- a/bin/manifest.js +++ b/bin/manifest.js @@ -1,11 +1,44 @@ // utils to generate a manifest.json file for a frames v2 app import { mnemonicToAccount } from 'viem/accounts'; +import dotenv from 'dotenv'; +import { fileURLToPath } from 'url'; +import { dirname, join } from 'path'; -export async function generateManifest(fid, seedPhrase) { - if (!Number.isInteger(fid) || fid <= 0) { - throw new Error('FID must be a positive integer'); +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +async function lookupFidByCustodyAddress(custodyAddress, projectPath) { + // Load environment variables from the project's .env file + dotenv.config({ path: join(projectPath, '.env') }); + + const apiKey = process.env.NEYNAR_API_KEY; + if (!apiKey) { + throw new Error('Neynar API key is required. Please set NEYNAR_API_KEY in your .env file'); } + const response = await fetch( + `https://api.neynar.com/v2/farcaster/user/custody-address?custody_address=${custodyAddress}`, + { + 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.user?.fid) { + throw new Error('No FID found for this custody address'); + } + + return data.user.fid; +} + +export async function generateManifest(seedPhrase, projectPath) { let account; try { account = mnemonicToAccount(seedPhrase); @@ -14,6 +47,9 @@ export async function generateManifest(fid, seedPhrase) { } const custodyAddress = account.address; + // Look up FID using custody address + const fid = await lookupFidByCustodyAddress(custodyAddress, projectPath); + const header = { fid, type: 'custody', // question: do we want to support type of 'app_key', which indicates the signature is from a registered App Key for the FID diff --git a/package-lock.json b/package-lock.json index 7f8d151..7f25aa1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@upstash/redis": "^1.34.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dotenv": "^16.4.7", "inquirer": "^12.4.3", "localtunnel": "^2.0.2", "lucide-react": "^0.469.0", @@ -4661,6 +4662,18 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/duplexify": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", diff --git a/package.json b/package.json index 1b8568d..f399eb9 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "@upstash/redis": "^1.34.3", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "dotenv": "^16.4.7", "inquirer": "^12.4.3", "localtunnel": "^2.0.2", "lucide-react": "^0.469.0", diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 382bd79..8a4d021 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -14,7 +14,12 @@ export default async function RootLayout({ }: Readonly<{ children: React.ReactNode; }>) { + console.log('Environment variables:'); + console.log('NEXT_PUBLIC_URL:', process.env.NEXT_PUBLIC_URL); + console.log('NEXTAUTH_URL:', process.env.NEXTAUTH_URL); + const session = await getSession() + console.log('Session:', session); return ( diff --git a/src/auth.ts b/src/auth.ts index 23dc1f3..6ace57d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -10,6 +10,21 @@ declare module "next-auth" { } } +function getDomainFromUrl(urlString: string | undefined): string { + if (!urlString) { + console.warn('NEXTAUTH_URL is not set, using localhost:3000 as fallback'); + return 'localhost:3000'; + } + try { + const url = new URL(urlString); + return url.hostname; + } catch (error) { + console.error('Invalid NEXTAUTH_URL:', urlString, error); + console.warn('Using localhost:3000 as fallback'); + return 'localhost:3000'; + } +} + export const authOptions: AuthOptions = { // Configure one or more authentication providers providers: [ @@ -47,11 +62,13 @@ export const authOptions: AuthOptions = { ethereum: viemConnector(), }); + const domain = getDomainFromUrl(process.env.NEXTAUTH_URL); + console.log('Using domain for auth:', domain); + const verifyResponse = await appClient.verifySignInMessage({ message: credentials?.message as string, signature: credentials?.signature as `0x${string}`, - // question: what domain should this be? - domain: new URL(process.env.NEXTAUTH_URL ?? '').hostname, + domain, nonce: csrfToken, }); const { success, fid } = verifyResponse;