diff --git a/bin/init.js b/bin/init.js index 0800764..59027ac 100644 --- a/bin/init.js +++ b/bin/init.js @@ -483,18 +483,19 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe }; 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", + "ts-node": "^10.9.2" }; // Add Neynar SDK if selected diff --git a/package.json b/package.json index d5f5e56..e83cc8e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.9", + "version": "1.6.0", "type": "module", "private": false, "access": "public", @@ -31,11 +31,11 @@ ], "scripts": { "dev": "node scripts/dev.js", - "build": "node scripts/build.js", + "build": "next build", "build:raw": "next build", "start": "next start", "lint": "next lint", - "deploy:vercel": "node scripts/deploy.js", + "deploy:vercel": "ts-node scripts/deploy.ts", "deploy:raw": "vercel --prod", "cleanup": "node scripts/cleanup.js" }, diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100755 index ac8ed77..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,359 +0,0 @@ -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 loadEnvLocal() { - try { - 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, - }, - ]); - - if (loadLocal) { - 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)) { - // 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"); - } - if (localEnv.SPONSOR_SIGNER) { - process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; - } - } - } catch (error) { - // Error reading .env.local, which is fine - 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, ".."); - -async function validateDomain(domain) { - // Remove http:// or https:// if present - 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"); - } - - return cleanDomain; -} - -async function queryNeynarApp(apiKey) { - if (!apiKey) { - return null; - } - try { - const response = await fetch( - `https://api.neynar.com/portal/app_by_api_key`, - { - headers: { - "x-api-key": apiKey, - }, - } - ); - const data = await response.json(); - return data; - } catch (error) { - console.error("Error querying Neynar app data:", error); - return null; - } -} - -async function generateFarcasterMetadata(domain, webhookUrl) { - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(","); - - return { - accountAssociation: { - header: "", - payload: "", - signature: "", - }, - frame: { - version: "1", - name: process.env.NEXT_PUBLIC_MINI_APP_NAME, - iconUrl: `https://${domain}/icon.png`, - homeUrl: `https://${domain}`, - imageUrl: `https://${domain}/api/opengraph-image`, - buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, - splashImageUrl: `https://${domain}/splash.png`, - splashBackgroundColor: "#f7f7f7", - webhookUrl, - description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, - primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, - tags, - }, - }; -} - -async function main() { - try { - 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):", - validate: async (input) => { - try { - await validateDomain(input); - return true; - } 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):", - default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: (input) => { - 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", - validate: (input) => { - if (input.trim() === "") { - return "Button text cannot be empty"; - } - return true; - }, - }, - ]); - - // Get Neynar configuration - let neynarApiKey = process.env.NEYNAR_API_KEY; - let neynarClientId = process.env.NEYNAR_CLIENT_ID; - let useNeynar = true; - - while (useNeynar) { - 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, - }, - ]); - neynarApiKey = inputNeynarApiKey; - } else { - console.log("Using existing Neynar API key from .env"); - } - - if (!neynarApiKey) { - useNeynar = false; - break; - } - - // Try to get client ID from API - if (!neynarClientId) { - const appInfo = await queryNeynarApp(neynarApiKey); - if (appInfo) { - neynarClientId = appInfo.app_uuid; - console.log("āœ… Fetched Neynar app client ID"); - break; - } - } - - // We have a client ID (either from .env or fetched from API), so we can break out of the loop - if (neynarClientId) { - break; - } - - // If we get here, the API key was invalid - 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, - }, - ]); - - // Reset for retry - neynarApiKey = null; - neynarClientId = null; - - if (!retry) { - useNeynar = false; - break; - } - } - - // Generate 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` - : `https://${domain}/api/webhook`; - - 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") - : ""; - - // Add or update environment variables - const newEnvVars = [ - // Base URL - `NEXT_PUBLIC_URL=https://${domain}`, - - // 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_BUTTON_TEXT="${buttonText}"`, - - // Analytics - `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.SPONSOR_SIGNER ? - [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] : []), - - // 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" - }"`, - - // NextAuth configuration - `NEXTAUTH_SECRET="${ - process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex") - }"`, - `NEXTAUTH_URL="https://${domain}"`, - - // Mini app manifest with signature - `MINI_APP_METADATA=${JSON.stringify(metadata)}`, - ]; - - // Filter out empty values and join with newlines - 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("="); - if (envContent.includes(`${key}=`)) { - envContent = envContent.replace(new RegExp(`${key}=.*`), varLine); - } else { - envContent += `\n${varLine}`; - } - }); - - // Write updated .env file - fs.writeFileSync(envPath, envContent); - - 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( - "\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); - process.exit(1); - } -} - -main(); diff --git a/scripts/deploy.js b/scripts/deploy.ts similarity index 70% rename from scripts/deploy.js rename to scripts/deploy.ts index 9c4ca4d..e64d4d2 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.ts @@ -7,6 +7,7 @@ import inquirer from 'inquirer'; import dotenv from 'dotenv'; import crypto from 'crypto'; import { Vercel } from '@vercel/sdk'; +import { APP_NAME, APP_BUTTON_TEXT } from '../src/lib/constants'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); @@ -14,32 +15,10 @@ const projectRoot = path.join(__dirname, '..'); // Load environment variables in specific order dotenv.config({ path: '.env' }); -async function generateFarcasterMetadata(domain, webhookUrl) { - const trimmedDomain = domain.trim(); - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); - - return { - frame: { - 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', - webhookUrl: webhookUrl?.trim(), - description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, - primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, - tags, - }, - }; -} - -async function loadEnvLocal() { +async function loadEnvLocal(): Promise { try { if (fs.existsSync('.env.local')) { - const { loadLocal } = await inquirer.prompt([ + const { loadLocal }: { loadLocal: boolean } = await inquirer.prompt([ { type: 'confirm', name: 'loadLocal', @@ -54,12 +33,7 @@ async function loadEnvLocal() { 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', + 'SEED_PHRASE', 'NEYNAR_API_KEY', 'NEYNAR_CLIENT_ID', 'SPONSOR_SIGNER', @@ -83,12 +57,12 @@ async function loadEnvLocal() { console.log('āœ… Values from .env.local have been written to .env'); } } - } catch (error) { + } catch (error: unknown) { console.log('Note: No .env.local file found'); } } -async function checkRequiredEnvVars() { +async function checkRequiredEnvVars(): Promise { console.log('\nšŸ“ Checking environment variables...'); console.log('Loading values from .env...'); @@ -98,17 +72,15 @@ async function checkRequiredEnvVars() { { 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', + default: APP_NAME, + validate: (input: string) => 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', - }, + default: APP_BUTTON_TEXT ?? 'Launch Mini App', + validate: (input: string) => input.trim() !== '' || 'Button text cannot be empty' + } ]; const missingVars = requiredVars.filter( @@ -182,39 +154,43 @@ async function checkRequiredEnvVars() { } } -async function getGitRemote() { +async function getGitRemote(): Promise { try { const remoteUrl = execSync('git remote get-url origin', { cwd: projectRoot, encoding: 'utf8', }).trim(); return remoteUrl; - } catch (error) { - return null; + } catch (error: unknown) { + if (error instanceof Error) { + return null; + } + throw error; } } -async function checkVercelCLI() { +async function checkVercelCLI(): Promise { try { - execSync('vercel --version', { - stdio: 'ignore', - shell: process.platform === 'win32', + execSync('vercel --version', { + stdio: 'ignore' }); return true; - } catch (error) { - return false; + } catch (error: unknown) { + if (error instanceof Error) { + return false; + } + throw error; } } -async function installVercelCLI() { +async function installVercelCLI(): Promise { console.log('Installing Vercel CLI...'); - execSync('npm install -g vercel', { - stdio: 'inherit', - shell: process.platform === 'win32', + execSync('npm install -g vercel', { + stdio: 'inherit' }); } -async function getVercelToken() { +async function getVercelToken(): Promise { try { // Try to get token from Vercel CLI config const configPath = path.join(os.homedir(), '.vercel', 'auth.json'); @@ -222,8 +198,10 @@ async function getVercelToken() { const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); return authConfig.token; } - } catch (error) { - console.warn('Could not read Vercel token from config file'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('Could not read Vercel token from config file'); + } } // Try environment variable @@ -242,14 +220,15 @@ async function getVercelToken() { // The token isn't directly exposed, so we'll need to use CLI for some operations 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.' - ); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error('Not logged in to Vercel CLI. Please run this script again to login.'); + } + throw error; } } -async function loginToVercel() { +async function loginToVercel(): Promise { console.log('\nšŸ”‘ Vercel Login'); console.log('You can either:'); console.log('1. Log in to an existing Vercel account'); @@ -267,7 +246,7 @@ async function loginToVercel() { stdio: 'inherit', }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { child.on('close', (code) => { resolve(); }); @@ -283,8 +262,8 @@ async function loginToVercel() { execSync('vercel whoami', { stdio: 'ignore' }); console.log('āœ… Successfully logged in to Vercel!'); return true; - } catch (error) { - if (error.message.includes('Account not found')) { + } catch (error: unknown) { + if (error instanceof Error && error.message.includes('Account not found')) { console.log('ā„¹ļø Waiting for Vercel account setup to complete...'); } await new Promise((resolve) => setTimeout(resolve, 2000)); @@ -298,9 +277,9 @@ async function loginToVercel() { return false; } -async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { +async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key: string, value: string | object): Promise { try { - let processedValue; + let processedValue: string; if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { @@ -312,8 +291,8 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { idOrName: projectId, }); - const existingVar = existingVars.envs?.find( - (env) => env.key === key && env.target?.includes('production') + const existingVar = existingVars.envs?.find((env: any) => + env.key === key && env.target?.includes('production') ); if (existingVar) { @@ -342,16 +321,16 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { } return true; - } catch (error) { - console.warn( - `āš ļø Warning: Failed to set environment variable ${key}:`, - error.message - ); - return false; + } catch (error: unknown) { + if (error instanceof Error) { + console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + return false; + } + throw error; } } -async function setVercelEnvVarCLI(key, value, projectRoot) { +async function setVercelEnvVarCLI(key: string, value: string | object, projectRoot: string): Promise { try { // Remove existing env var try { @@ -360,11 +339,11 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { stdio: 'ignore', env: process.env, }); - } catch (error) { + } catch (error: unknown) { // Ignore errors from removal } - let processedValue; + let processedValue: string; if (typeof value === 'object') { processedValue = JSON.stringify(value); } else { @@ -376,7 +355,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { fs.writeFileSync(tempFilePath, processedValue, 'utf8'); // Use appropriate command based on platform - let command; + let command: string; if (process.platform === 'win32') { command = `type "${tempFilePath}" | vercel env add ${key} production`; } else { @@ -386,36 +365,30 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { execSync(command, { cwd: projectRoot, stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts - shell: true, - env: process.env, + env: process.env }); fs.unlinkSync(tempFilePath); console.log(`āœ… Set environment variable: ${key}`); return true; - } catch (error) { + } catch (error: unknown) { const tempFilePath = path.join(projectRoot, `${key}_temp.txt`); if (fs.existsSync(tempFilePath)) { fs.unlinkSync(tempFilePath); } - console.warn( - `āš ļø Warning: Failed to set environment variable ${key}:`, - error.message - ); - return false; + if (error instanceof Error) { + console.warn(`āš ļø Warning: Failed to set environment variable ${key}:`, error.message); + return false; + } + throw error; } } -async function setEnvironmentVariables( - vercelClient, - projectId, - envVars, - projectRoot -) { +async function setEnvironmentVariables(vercelClient: Vercel | null, projectId: string | null, envVars: Record, projectRoot: string): Promise> { console.log('\nšŸ“ Setting up environment variables...'); - - const results = []; - + + const results: Array<{ key: string; success: boolean }> = []; + for (const [key, value] of Object.entries(envVars)) { if (!value) continue; @@ -447,23 +420,18 @@ async function setEnvironmentVariables( return results; } -async function waitForDeployment( - vercelClient, - projectId, - maxWaitTime = 300000 -) { - // 5 minutes +async function waitForDeployment(vercelClient: Vercel | null, projectId: string, maxWaitTime = 300000): Promise { // 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({ + const deployments = await vercelClient?.deployments.list({ projectId: projectId, limit: 1, }); - - if (deployments.deployments?.[0]) { + + if (deployments?.deployments?.[0]) { const deployment = deployments.deployments[0]; console.log(`šŸ“Š Deployment status: ${deployment.state}`); @@ -482,16 +450,19 @@ async function waitForDeployment( console.log('ā³ No deployment found yet, waiting...'); 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)); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not check deployment status:', error.message); + await new Promise(resolve => setTimeout(resolve, 5000)); + } + throw error; } } throw new Error('Deployment timed out after 5 minutes'); } -async function deployToVercel(useGitHub = false) { +async function deployToVercel(useGitHub = false): Promise { try { console.log('\nšŸš€ Deploying to Vercel...'); @@ -523,19 +494,19 @@ async function deployToVercel(useGitHub = false) { // Use spawn instead of execSync for better error handling const { spawn } = await import('child_process'); - const vercelSetup = spawn('vercel', [], { - cwd: projectRoot, - stdio: 'inherit', - shell: process.platform === 'win32', - }); + const vercelSetup = spawn('vercel', [], { + cwd: projectRoot, + stdio: 'inherit', + shell: process.platform === 'win32' ? true : undefined + }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelSetup.on('close', (code) => { if (code === 0 || code === null) { console.log('āœ… Vercel project setup completed'); resolve(); } else { - console.log('āš ļø Vercel setup command completed (this is normal)'); + console.log('āš ļø Vercel setup command completed (this is normal)'); resolve(); // Don't reject, as this is often expected } }); @@ -550,38 +521,40 @@ async function deployToVercel(useGitHub = false) { await new Promise((resolve) => setTimeout(resolve, 2000)); // Load project info - let projectId; + let projectId: string; try { 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.' - ); + } catch (error: unknown) { + if (error instanceof Error) { + throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.'); + } + throw error; } // Get Vercel token and initialize SDK client - let vercelClient = null; + let vercelClient: Vercel | null = null; try { const token = await getVercelToken(); if (token) { vercelClient = new Vercel({ - bearerToken: token, + bearerToken: token }); console.log('āœ… Initialized Vercel SDK client'); } - } catch (error) { - console.warn( - 'āš ļø Could not initialize Vercel SDK, falling back to CLI operations' - ); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not initialize Vercel SDK, falling back to CLI operations'); + } + throw error; } // Get project details console.log('\nšŸ” Getting project details...'); - let domain; - let projectName; + let domain: string | undefined; + let projectName: string | undefined; if (vercelClient) { try { @@ -591,10 +564,11 @@ async function deployToVercel(useGitHub = false) { projectName = project.name; domain = `${projectName}.vercel.app`; console.log('🌐 Using project name for domain:', domain); - } catch (error) { - console.warn( - 'āš ļø Could not get project details via SDK, using CLI fallback' - ); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not get project details via SDK, using CLI fallback'); + } + throw error; } } @@ -629,25 +603,17 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Using fallback domain:', domain); } } - } catch (error) { - console.warn('āš ļø Could not inspect project, using fallback domain'); - // Use a fallback domain based on project ID - domain = `project-${projectId.slice(-8)}.vercel.app`; - console.log('🌐 Using fallback domain:', domain); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not inspect project, using fallback domain'); + // Use a fallback domain based on project ID + domain = `project-${projectId.slice(-8)}.vercel.app`; + console.log('🌐 Using fallback domain:', domain); + } + throw error; } } - // 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`; - - 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'); @@ -656,18 +622,11 @@ async function deployToVercel(useGitHub = false) { 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.SPONSOR_SIGNER && { - SPONSOR_SIGNER: process.env.SPONSOR_SIGNER, - }), - ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }), - + + ...(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 }), + ...Object.fromEntries( Object.entries(process.env).filter(([key]) => key.startsWith('NEXT_PUBLIC_') @@ -703,7 +662,7 @@ async function deployToVercel(useGitHub = false) { env: process.env, }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelDeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Vercel deployment command completed'); @@ -721,16 +680,16 @@ async function deployToVercel(useGitHub = false) { }); // Wait for deployment to actually complete - let deployment; + let deployment: any; if (vercelClient) { try { deployment = await waitForDeployment(vercelClient, projectId); - } catch (error) { - console.warn( - 'āš ļø Could not verify deployment completion:', - error.message - ); - console.log('ā„¹ļø Proceeding with domain verification...'); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not verify deployment completion:', error.message); + console.log('ā„¹ļø Proceeding with domain verification...'); + } + throw error; } } @@ -741,11 +700,12 @@ async function deployToVercel(useGitHub = false) { if (vercelClient && deployment) { try { actualDomain = deployment.url || domain; - console.log('🌐 Verified actual domain:', actualDomain); - } catch (error) { - console.warn( - 'āš ļø Could not verify domain via SDK, using assumed domain' - ); + console.log('🌐 Verified actual domain:', actualDomain); + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not verify domain via SDK, using assumed domain'); + } + throw error; } } @@ -753,33 +713,12 @@ async function deployToVercel(useGitHub = false) { 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`; - - const updatedEnv = { + const updatedEnv: Record = { NEXTAUTH_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; - } - - 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'], { @@ -788,7 +727,7 @@ async function deployToVercel(useGitHub = false) { env: process.env, }); - await new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { vercelRedeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Redeployment completed'); @@ -810,16 +749,58 @@ async function deployToVercel(useGitHub = false) { 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šŸ“ You can manage your project at https://vercel.com/dashboard'); + + // Prompt user to sign manifest in browser and paste accountAssociation + console.log(`\nāš ļø To complete your mini app manifest, you must sign it using the Farcaster developer portal.`); + console.log('1. Go to: https://farcaster.xyz/~/developers/mini-apps/manifest?domain=' + domain); + console.log('2. Click "Transfer Ownership" and follow the instructions to sign the manifest.'); + console.log('3. Copy the resulting accountAssociation JSON from the browser.'); + console.log('4. Paste it below when prompted.'); + + const { userAccountAssociation } = await inquirer.prompt([ + { + type: 'editor', + name: 'userAccountAssociation', + message: 'Paste the accountAssociation JSON here:', + validate: (input: string) => { + try { + const parsed = JSON.parse(input); + if (parsed.header && parsed.payload && parsed.signature) { + return true; + } + return 'Invalid accountAssociation: must have header, payload, and signature'; + } catch (e) { + return 'Invalid JSON'; + } + } + } + ]); + const parsedAccountAssociation = JSON.parse(userAccountAssociation); + + // Write APP_ACCOUNT_ASSOCIATION to src/lib/constants.ts + const constantsPath = path.join(projectRoot, 'src', 'lib', 'constants.ts'); + let constantsContent = fs.readFileSync(constantsPath, 'utf8'); + + // Replace the APP_ACCOUNT_ASSOCIATION line using a robust, anchored, multiline regex + const newAccountAssociation = `export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = ${JSON.stringify(parsedAccountAssociation, null, 2)};`; + constantsContent = constantsContent.replace( + /^export const APP_ACCOUNT_ASSOCIATION\s*:\s*AccountAssociation \| undefined\s*=\s*[^;]*;/m, + newAccountAssociation ); - } catch (error) { - console.error('\nāŒ Deployment failed:', error.message); - process.exit(1); + fs.writeFileSync(constantsPath, constantsContent); + console.log('\nāœ… APP_ACCOUNT_ASSOCIATION updated in src/lib/constants.ts'); + + } catch (error: unknown) { + if (error instanceof Error) { + console.error('\nāŒ Deployment failed:', error.message); + process.exit(1); + } + throw error; } } -async function main() { +async function main(): Promise { try { console.log('šŸš€ Vercel Mini App Deployment (SDK Edition)'); console.log( @@ -834,13 +815,16 @@ async function main() { // Check if @vercel/sdk is installed try { await import('@vercel/sdk'); - } catch (error) { - console.log('šŸ“¦ Installing @vercel/sdk...'); - execSync('npm install @vercel/sdk', { - cwd: projectRoot, - stdio: 'inherit', - }); - console.log('āœ… @vercel/sdk installed successfully'); + } catch (error: unknown) { + if (error instanceof Error) { + console.log('šŸ“¦ Installing @vercel/sdk...'); + execSync('npm install @vercel/sdk', { + cwd: projectRoot, + stdio: 'inherit' + }); + console.log('āœ… @vercel/sdk installed successfully'); + } + throw error; } await checkRequiredEnvVars(); @@ -896,9 +880,13 @@ async function main() { } await deployToVercel(useGitHub); - } catch (error) { - console.error('\nāŒ Error:', error.message); - process.exit(1); + + } catch (error: unknown) { + if (error instanceof Error) { + console.error('\nāŒ Error:', error.message); + process.exit(1); + } + throw error; } } diff --git a/src/app/.well-known/farcaster.json/route.ts b/src/app/.well-known/farcaster.json/route.ts index 19ce6d7..d116c4f 100644 --- a/src/app/.well-known/farcaster.json/route.ts +++ b/src/app/.well-known/farcaster.json/route.ts @@ -1,9 +1,9 @@ import { NextResponse } from 'next/server'; -import { getFarcasterMetadata } from '../../../lib/utils'; +import { getFarcasterDomainManifest } from '~/lib/utils'; export async function GET() { try { - const config = await getFarcasterMetadata(); + const config = await getFarcasterDomainManifest(); return NextResponse.json(config); } catch (error) { console.error('Error generating metadata:', error); diff --git a/src/components/ui/Share.tsx b/src/components/ui/Share.tsx index ef29ebd..2807032 100644 --- a/src/components/ui/Share.tsx +++ b/src/components/ui/Share.tsx @@ -4,6 +4,7 @@ import { useCallback, useState, useEffect } from 'react'; import { Button } from './Button'; import { useMiniApp } from '@neynar/react'; import { type ComposeCast } from "@farcaster/miniapp-sdk"; +import { APP_URL } from '~/lib/constants'; interface EmbedConfig { path?: string; @@ -72,7 +73,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals return embed; } if (embed.path) { - const baseUrl = process.env.NEXT_PUBLIC_URL || window.location.origin; + const baseUrl = APP_URL || window.location.origin; const url = new URL(`${baseUrl}${embed.path}`); // Add UTM parameters diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 789c5b8..4c345cc 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -6,6 +6,7 @@ import { ShareButton } from "../Share"; import { Button } from "../Button"; import { SignIn } from "../wallet/SignIn"; import { type Haptics } from "@farcaster/miniapp-sdk"; +import { APP_URL } from "~/lib/constants"; import { NeynarAuthButton } from '../NeynarAuthButton/index'; /** @@ -96,7 +97,7 @@ export function ActionsTab() { */ const copyUserShareUrl = useCallback(async () => { if (context?.user?.fid) { - const userShareUrl = `${process.env.NEXT_PUBLIC_URL}/share/${context.user.fid}`; + const userShareUrl = `${APP_URL}/share/${context.user.fid}`; await navigator.clipboard.writeText(userShareUrl); setNotificationState((prev) => ({ ...prev, shareUrlCopied: true })); setTimeout( @@ -130,9 +131,7 @@ export function ActionsTab() { cast={{ text: 'Check out this awesome frame @1 @2 @3! šŸš€šŸŖ', bestFriends: true, - embeds: [ - `${process.env.NEXT_PUBLIC_URL}/share/${context?.user?.fid || ''}`, - ], + embeds: [`${APP_URL}/share/${context?.user?.fid || ''}`] }} className='w-full' /> diff --git a/src/lib/constants.ts b/src/lib/constants.ts index 7a7661b..dc05838 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,5 @@ +import { type AccountAssociation } from '@farcaster/miniapp-node'; + /** * Application constants and configuration values. * @@ -14,63 +16,70 @@ * The base URL of the application. * Used for generating absolute URLs for assets and API endpoints. */ -export const APP_URL = process.env.NEXT_PUBLIC_URL!; +export const APP_URL: string = 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 = 'shreyas-testing-mini-app'; +export const APP_NAME: string = 'Starter Kit'; /** * A brief description of the mini app's functionality. * Used in app store listings and metadata. */ -export const APP_DESCRIPTION = 'A Farcaster mini app created with Neynar'; +export const APP_DESCRIPTION: string = 'A demo of the Neynar Starter Kit'; /** * The primary category for the mini app. * Used for app store categorization and discovery. */ -export const APP_PRIMARY_CATEGORY = ''; +export const APP_PRIMARY_CATEGORY: string = 'developer-tools'; /** * Tags associated with the mini app. * Used for search and discovery in app stores. */ -export const APP_TAGS = ['neynar', 'starter-kit', 'demo']; +export const APP_TAGS: string[] = ['neynar', 'starter-kit', 'demo']; // --- Asset URLs --- /** * URL for the app's icon image. * Used in app store listings and UI elements. */ -export const APP_ICON_URL = `${APP_URL}/icon.png`; +export const APP_ICON_URL: string = `${APP_URL}/icon.png`; /** * URL for the app's Open Graph image. * Used for social media sharing and previews. */ -export const APP_OG_IMAGE_URL = `${APP_URL}/api/opengraph-image`; +export const APP_OG_IMAGE_URL: string = `${APP_URL}/api/opengraph-image`; /** * URL for the app's splash screen image. * Displayed during app loading. */ -export const APP_SPLASH_URL = `${APP_URL}/splash.png`; +export const APP_SPLASH_URL: string = `${APP_URL}/splash.png`; /** * Background color for the splash screen. * Used as fallback when splash image is loading. */ -export const APP_SPLASH_BACKGROUND_COLOR = '#f7f7f7'; +export const APP_SPLASH_BACKGROUND_COLOR: string = "#f7f7f7"; + +/** + * Account association for the mini app. + * Used to associate the mini app with a Farcaster account. + * If not provided, the mini app will be unsigned and have limited capabilities. + */ +export const APP_ACCOUNT_ASSOCIATION: AccountAssociation | undefined = undefined; // --- 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 Mini App'; +export const APP_BUTTON_TEXT: string = 'Launch NSK'; // --- Integration Configuration --- /** @@ -80,8 +89,7 @@ export const APP_BUTTON_TEXT = 'Launch Mini App'; * 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: string = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` : `${APP_URL}/api/webhook`; @@ -92,7 +100,7 @@ export const APP_WEBHOOK_URL = * When false, wallet functionality is completely hidden from the UI. * Useful for mini apps that don't require wallet integration. */ -export const USE_WALLET = true; +export const USE_WALLET: boolean = true; /** * Flag to enable/disable analytics tracking. @@ -101,7 +109,7 @@ export const USE_WALLET = true; * When false, analytics collection is disabled. * Useful for privacy-conscious users or development environments. */ -export const ANALYTICS_ENABLED = true; +export const ANALYTICS_ENABLED: boolean = true; // PLEASE DO NOT UPDATE THIS export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 0762d67..59430e8 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,6 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; -import { mnemonicToAccount } from "viem/accounts"; +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; +import { type Manifest } from '@farcaster/miniapp-node'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, @@ -9,35 +9,11 @@ import { APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, - APP_TAGS, - APP_URL, + APP_SPLASH_URL, + APP_TAGS, APP_URL, APP_WEBHOOK_URL, -} from "./constants"; -import { APP_SPLASH_URL } from "./constants"; - -interface MiniAppMetadata { - version: string; - name: string; - iconUrl: string; - homeUrl: string; - imageUrl?: string; - buttonTitle?: string; - splashImageUrl?: string; - splashBackgroundColor?: string; - webhookUrl?: string; - description?: string; - primaryCategory?: string; - tags?: string[]; -} - -interface MiniAppManifest { - accountAssociation?: { - header: string; - payload: string; - signature: string; - }; - frame: MiniAppMetadata; -} + APP_ACCOUNT_ASSOCIATION, +} from './constants'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -64,36 +40,10 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) { }; } -export async function getFarcasterMetadata(): Promise { - // First check for MINI_APP_METADATA in .env and use that if it exists - 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"); - return metadata; - } catch (error) { - console.warn( - "Failed to parse MINI_APP_METADATA from environment:", - error - ); - } - } - - if (!APP_URL) { - 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); - +export async function getFarcasterDomainManifest(): Promise { return { - accountAssociation: { - header: "", - payload: "", - signature: "", - }, - frame: { + accountAssociation: APP_ACCOUNT_ASSOCIATION, + miniapp: { version: "1", name: APP_NAME ?? "Neynar Starter Kit", iconUrl: APP_ICON_URL,