diff --git a/bin/init.js b/bin/init.js index 16cb38e..a755168 100644 --- a/bin/init.js +++ b/bin/init.js @@ -488,18 +488,19 @@ export async function init( }; 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 @@ -550,19 +551,19 @@ export async function init( return content; }; - // Regex patterns that match whole lines with export const + // Regex patterns that match whole lines with export const (with TypeScript types) const patterns = { - APP_NAME: /^export const APP_NAME\s*=\s*['"`][^'"`]*['"`];$/m, + APP_NAME: /^export const APP_NAME\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m, APP_DESCRIPTION: - /^export const APP_DESCRIPTION\s*=\s*['"`][^'"`]*['"`];$/m, + /^export const APP_DESCRIPTION\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m, APP_PRIMARY_CATEGORY: - /^export const APP_PRIMARY_CATEGORY\s*=\s*['"`][^'"`]*['"`];$/m, - APP_TAGS: /^export const APP_TAGS\s*=\s*\[[^\]]*\];$/m, + /^export const APP_PRIMARY_CATEGORY\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m, + APP_TAGS: /^export const APP_TAGS\s*:\s*string\[\]\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, + /^export const APP_BUTTON_TEXT\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m, + USE_WALLET: /^export const USE_WALLET\s*:\s*boolean\s*=\s*(true|false);$/m, ANALYTICS_ENABLED: - /^export const ANALYTICS_ENABLED\s*=\s*(true|false);$/m, + /^export const ANALYTICS_ENABLED\s*:\s*boolean\s*=\s*(true|false);$/m, }; // Update APP_NAME diff --git a/package.json b/package.json index ca7e2eb..46bbf50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.11", + "version": "1.6.2", "type": "module", "private": false, "access": "public", @@ -31,16 +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", - "lint:fix": "next lint --fix", - "format": "prettier --write .", - "format:check": "prettier --check .", - "format:fix": "prettier --write . && eslint --fix . --max-warnings 50", - "typecheck": "tsc --noEmit", - "deploy:vercel": "node scripts/deploy.js", + "deploy:vercel": "ts-node scripts/deploy.ts", "deploy:raw": "vercel --prod", "cleanup": "node scripts/cleanup.js" }, @@ -55,11 +50,6 @@ "devDependencies": { "@neynar/nodejs-sdk": "^2.19.0", "@types/node": "^22.13.10", - "@typescript-eslint/eslint-plugin": "^8.35.1", - "@typescript-eslint/parser": "^8.35.1", - "eslint": "^8.57.0", - "eslint-config-next": "^15.0.0", - "prettier": "^3.3.3", "typescript": "^5.6.3" } } diff --git a/scripts/build.js b/scripts/build.js deleted file mode 100755 index f6584b9..0000000 --- a/scripts/build.js +++ /dev/null @@ -1,361 +0,0 @@ -import { execSync } from 'child_process'; -import crypto from 'crypto'; -import fs from 'fs'; -import path from 'path'; -import { fileURLToPath } from 'url'; -import dotenv from 'dotenv'; -import inquirer from 'inquirer'; - -// 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, - }, - ]); - - const localEnv = dotenv.parse(fs.readFileSync('.env.local')); - - if (loadLocal) { - console.log('Loading values from .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 66% rename from scripts/deploy.js rename to scripts/deploy.ts index 78e4241..cfc629d 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.ts @@ -1,12 +1,13 @@ import { execSync, spawn } from 'child_process'; -import crypto from 'crypto'; import fs from 'fs'; -import os from 'os'; import path from 'path'; +import os from 'os'; import { fileURLToPath } from 'url'; -import { Vercel } from '@vercel/sdk'; -import dotenv from 'dotenv'; 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,20 +72,19 @@ 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( - varConfig => !process.env[varConfig.name], + (varConfig) => !process.env[varConfig.name] ); if (missingVars.length > 0) { @@ -137,7 +110,7 @@ async function checkRequiredEnvVars() { const newLine = envContent ? '\n' : ''; fs.appendFileSync( '.env', - `${newLine}${varConfig.name}="${value.trim()}"`, + `${newLine}${varConfig.name}="${value.trim()}"` ); } @@ -160,7 +133,7 @@ async function checkRequiredEnvVars() { if (storeSeedPhrase) { fs.appendFileSync( '.env.local', - `\nSPONSOR_SIGNER="${sponsorSigner}"`, + `\nSPONSOR_SIGNER="${sponsorSigner}"` ); console.log('āœ… Sponsor signer preference stored in .env.local'); } @@ -181,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'); @@ -221,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 @@ -241,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'); @@ -259,22 +239,22 @@ async function loginToVercel() { 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', }); - await new Promise((resolve, reject) => { - child.on('close', code => { + await new Promise((resolve, reject) => { + 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.", + "If you're creating a new account, please complete the Vercel account setup in your browser first." ); for (let i = 0; i < 150; i++) { @@ -282,11 +262,11 @@ 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)); + await new Promise((resolve) => setTimeout(resolve, 2000)); } } @@ -297,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 { @@ -311,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) { @@ -341,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 { @@ -359,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 { @@ -375,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 { @@ -385,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; @@ -434,35 +408,30 @@ async function setEnvironmentVariables( } // 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}`)); + 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: 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}`); @@ -476,21 +445,24 @@ async function waitForDeployment( } // 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)); + } + } catch (error: unknown) { + if (error instanceof Error) { + console.warn('āš ļø Could not check deployment status:', error.message); 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)); + 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...'); @@ -506,81 +478,83 @@ async function deployToVercel(useGitHub = false) { framework: 'nextjs', }, null, - 2, - ), + 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', + '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', + '\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', [], { - 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) => { - vercelSetup.on('close', code => { + 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 } }); - vercelSetup.on('error', error => { + vercelSetup.on('error', (error) => { console.log('āš ļø Vercel setup command completed (this is normal)'); resolve(); // Don't reject, as this is often expected }); }); // 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; + let projectId: string; try { const projectJson = JSON.parse( - fs.readFileSync('.vercel/project.json', 'utf8'), + 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 { @@ -590,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; } } @@ -605,7 +580,7 @@ async function deployToVercel(useGitHub = false) { { cwd: projectRoot, encoding: 'utf8', - }, + } ); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); @@ -621,32 +596,24 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Using project name for domain:', domain); } else { console.warn( - 'āš ļø Could not determine project name from inspection, using fallback', + 'āš ļø 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); } } - } 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'); @@ -655,22 +622,15 @@ 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_'), - ), + key.startsWith('NEXT_PUBLIC_') + ) ), }; @@ -679,7 +639,7 @@ async function deployToVercel(useGitHub = false) { vercelClient, projectId, vercelEnv, - projectRoot, + projectRoot ); // Deploy the project @@ -702,8 +662,8 @@ async function deployToVercel(useGitHub = false) { env: process.env, }); - await new Promise((resolve, reject) => { - vercelDeploy.on('close', code => { + await new Promise((resolve, reject) => { + vercelDeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Vercel deployment command completed'); resolve(); @@ -713,23 +673,23 @@ async function deployToVercel(useGitHub = false) { } }); - vercelDeploy.on('error', error => { + vercelDeploy.on('error', (error) => { console.error('āŒ Vercel deployment error:', error.message); reject(error); }); }); // 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; } } @@ -740,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; } } @@ -752,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'], { @@ -787,8 +727,8 @@ async function deployToVercel(useGitHub = false) { env: process.env, }); - await new Promise((resolve, reject) => { - vercelRedeploy.on('close', code => { + await new Promise((resolve, reject) => { + vercelRedeploy.on('close', (code) => { if (code === 0) { console.log('āœ… Redeployment completed'); resolve(); @@ -798,7 +738,7 @@ async function deployToVercel(useGitHub = false) { } }); - vercelRedeploy.on('error', error => { + vercelRedeploy.on('error', (error) => { console.error('āŒ Redeployment error:', error.message); reject(error); }); @@ -809,20 +749,62 @@ 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( - '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'); @@ -833,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(); @@ -895,10 +880,14 @@ 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; } } -main(); +main(); \ No newline at end of file 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/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index 5a11a50..a0a6777 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -118,7 +118,7 @@ export function AuthDialog({ const content = getStepContent(); return ( -
+

diff --git a/src/components/ui/Share.tsx b/src/components/ui/Share.tsx index d44a442..aede901 100644 --- a/src/components/ui/Share.tsx +++ b/src/components/ui/Share.tsx @@ -1,9 +1,10 @@ 'use client'; import { useCallback, useState, useEffect } from 'react'; -import { type ComposeCast } from '@farcaster/miniapp-sdk'; -import { useMiniApp } from '@neynar/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; @@ -79,8 +80,7 @@ export function ShareButton({ 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 16abeb8..ec85e85 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -3,6 +3,7 @@ import { useCallback, useState } from 'react'; import { type Haptics } from '@farcaster/miniapp-sdk'; import { useMiniApp } from '@neynar/react'; +import { APP_URL } from '~/lib/constants'; import { Button } from '../Button'; import { NeynarAuthButton } from '../NeynarAuthButton/index'; import { ShareButton } from '../Share'; @@ -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 2b1577e..7bbea8e 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,5 +1,6 @@ import { type ClassValue, clsx } from 'clsx'; import { twMerge } from 'tailwind-merge'; +import { type Manifest } from '@farcaster/miniapp-node'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, @@ -8,35 +9,12 @@ import { APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, + APP_SPLASH_URL, APP_TAGS, APP_URL, APP_WEBHOOK_URL, + APP_ACCOUNT_ASSOCIATION, } 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; -} export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -63,31 +41,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); - 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'); - } - +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,