Merge branch 'main' into shreyas-formatting

This commit is contained in:
Shreyaschorge 2025-07-15 00:00:11 +05:30
commit 3815f45b44
No known key found for this signature in database
10 changed files with 296 additions and 713 deletions

View File

@ -488,18 +488,19 @@ export async function init(
}; };
packageJson.devDependencies = { packageJson.devDependencies = {
'@types/node': '^20', "@types/node": "^20",
'@types/react': '^19', "@types/react": "^19",
'@types/react-dom': '^19', "@types/react-dom": "^19",
'@vercel/sdk': '^1.9.0', "@vercel/sdk": "^1.9.0",
crypto: '^1.0.1', "crypto": "^1.0.1",
eslint: '^8', "eslint": "^8",
'eslint-config-next': '15.0.3', "eslint-config-next": "15.0.3",
localtunnel: '^2.0.2', "localtunnel": "^2.0.2",
'pino-pretty': '^13.0.0', "pino-pretty": "^13.0.0",
postcss: '^8', "postcss": "^8",
tailwindcss: '^3.4.1', "tailwindcss": "^3.4.1",
typescript: '^5', "typescript": "^5",
"ts-node": "^10.9.2"
}; };
// Add Neynar SDK if selected // Add Neynar SDK if selected
@ -550,19 +551,19 @@ export async function init(
return content; 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 = { 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: APP_DESCRIPTION:
/^export const APP_DESCRIPTION\s*=\s*['"`][^'"`]*['"`];$/m, /^export const APP_DESCRIPTION\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m,
APP_PRIMARY_CATEGORY: APP_PRIMARY_CATEGORY:
/^export const APP_PRIMARY_CATEGORY\s*=\s*['"`][^'"`]*['"`];$/m, /^export const APP_PRIMARY_CATEGORY\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m,
APP_TAGS: /^export const APP_TAGS\s*=\s*\[[^\]]*\];$/m, APP_TAGS: /^export const APP_TAGS\s*:\s*string\[\]\s*=\s*\[[^\]]*\];$/m,
APP_BUTTON_TEXT: APP_BUTTON_TEXT:
/^export const APP_BUTTON_TEXT\s*=\s*['"`][^'"`]*['"`];$/m, /^export const APP_BUTTON_TEXT\s*:\s*string\s*=\s*['"`][^'"`]*['"`];$/m,
USE_WALLET: /^export const USE_WALLET\s*=\s*(true|false);$/m, USE_WALLET: /^export const USE_WALLET\s*:\s*boolean\s*=\s*(true|false);$/m,
ANALYTICS_ENABLED: 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 // Update APP_NAME

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.5.11", "version": "1.6.2",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",
@ -31,16 +31,11 @@
], ],
"scripts": { "scripts": {
"dev": "node scripts/dev.js", "dev": "node scripts/dev.js",
"build": "node scripts/build.js", "build": "next build",
"build:raw": "next build", "build:raw": "next build",
"start": "next start", "start": "next start",
"lint": "next lint", "lint": "next lint",
"lint:fix": "next lint --fix", "deploy:vercel": "ts-node scripts/deploy.ts",
"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:raw": "vercel --prod", "deploy:raw": "vercel --prod",
"cleanup": "node scripts/cleanup.js" "cleanup": "node scripts/cleanup.js"
}, },
@ -55,11 +50,6 @@
"devDependencies": { "devDependencies": {
"@neynar/nodejs-sdk": "^2.19.0", "@neynar/nodejs-sdk": "^2.19.0",
"@types/node": "^22.13.10", "@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" "typescript": "^5.6.3"
} }
} }

View File

@ -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();

View File

@ -1,12 +1,13 @@
import { execSync, spawn } from 'child_process'; import { execSync, spawn } from 'child_process';
import crypto from 'crypto';
import fs from 'fs'; import fs from 'fs';
import os from 'os';
import path from 'path'; import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
import { Vercel } from '@vercel/sdk';
import dotenv from 'dotenv';
import inquirer from 'inquirer'; 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 __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..'); const projectRoot = path.join(__dirname, '..');
@ -14,32 +15,10 @@ const projectRoot = path.join(__dirname, '..');
// Load environment variables in specific order // Load environment variables in specific order
dotenv.config({ path: '.env' }); dotenv.config({ path: '.env' });
async function generateFarcasterMetadata(domain, webhookUrl) { async function loadEnvLocal(): Promise<void> {
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() {
try { try {
if (fs.existsSync('.env.local')) { if (fs.existsSync('.env.local')) {
const { loadLocal } = await inquirer.prompt([ const { loadLocal }: { loadLocal: boolean } = await inquirer.prompt([
{ {
type: 'confirm', type: 'confirm',
name: 'loadLocal', name: 'loadLocal',
@ -54,12 +33,7 @@ async function loadEnvLocal() {
const localEnv = dotenv.parse(fs.readFileSync('.env.local')); const localEnv = dotenv.parse(fs.readFileSync('.env.local'));
const allowedVars = [ const allowedVars = [
'NEXT_PUBLIC_MINI_APP_NAME', 'SEED_PHRASE',
'NEXT_PUBLIC_MINI_APP_DESCRIPTION',
'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY',
'NEXT_PUBLIC_MINI_APP_TAGS',
'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT',
'NEXT_PUBLIC_ANALYTICS_ENABLED',
'NEYNAR_API_KEY', 'NEYNAR_API_KEY',
'NEYNAR_CLIENT_ID', 'NEYNAR_CLIENT_ID',
'SPONSOR_SIGNER', 'SPONSOR_SIGNER',
@ -83,12 +57,12 @@ async function loadEnvLocal() {
console.log('✅ Values from .env.local have been written to .env'); console.log('✅ Values from .env.local have been written to .env');
} }
} }
} catch (error) { } catch (error: unknown) {
console.log('Note: No .env.local file found'); console.log('Note: No .env.local file found');
} }
} }
async function checkRequiredEnvVars() { async function checkRequiredEnvVars(): Promise<void> {
console.log('\n📝 Checking environment variables...'); console.log('\n📝 Checking environment variables...');
console.log('Loading values from .env...'); console.log('Loading values from .env...');
@ -98,20 +72,19 @@ async function checkRequiredEnvVars() {
{ {
name: 'NEXT_PUBLIC_MINI_APP_NAME', name: 'NEXT_PUBLIC_MINI_APP_NAME',
message: 'Enter the name for your frame (e.g., My Cool Mini App):', message: 'Enter the name for your frame (e.g., My Cool Mini App):',
default: process.env.NEXT_PUBLIC_MINI_APP_NAME, default: APP_NAME,
validate: input => input.trim() !== '' || 'Mini app name cannot be empty', validate: (input: string) => input.trim() !== '' || 'Mini app name cannot be empty'
}, },
{ {
name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT',
message: 'Enter the text for your frame button:', message: 'Enter the text for your frame button:',
default: default: APP_BUTTON_TEXT ?? 'Launch Mini App',
process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', validate: (input: string) => input.trim() !== '' || 'Button text cannot be empty'
validate: input => input.trim() !== '' || 'Button text cannot be empty', }
},
]; ];
const missingVars = requiredVars.filter( const missingVars = requiredVars.filter(
varConfig => !process.env[varConfig.name], (varConfig) => !process.env[varConfig.name]
); );
if (missingVars.length > 0) { if (missingVars.length > 0) {
@ -137,7 +110,7 @@ async function checkRequiredEnvVars() {
const newLine = envContent ? '\n' : ''; const newLine = envContent ? '\n' : '';
fs.appendFileSync( fs.appendFileSync(
'.env', '.env',
`${newLine}${varConfig.name}="${value.trim()}"`, `${newLine}${varConfig.name}="${value.trim()}"`
); );
} }
@ -160,7 +133,7 @@ async function checkRequiredEnvVars() {
if (storeSeedPhrase) { if (storeSeedPhrase) {
fs.appendFileSync( fs.appendFileSync(
'.env.local', '.env.local',
`\nSPONSOR_SIGNER="${sponsorSigner}"`, `\nSPONSOR_SIGNER="${sponsorSigner}"`
); );
console.log('✅ Sponsor signer preference stored in .env.local'); console.log('✅ Sponsor signer preference stored in .env.local');
} }
@ -181,39 +154,43 @@ async function checkRequiredEnvVars() {
} }
} }
async function getGitRemote() { async function getGitRemote(): Promise<string | null> {
try { try {
const remoteUrl = execSync('git remote get-url origin', { const remoteUrl = execSync('git remote get-url origin', {
cwd: projectRoot, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
}).trim(); }).trim();
return remoteUrl; return remoteUrl;
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
return null; return null;
} }
throw error;
}
} }
async function checkVercelCLI() { async function checkVercelCLI(): Promise<boolean> {
try { try {
execSync('vercel --version', { execSync('vercel --version', {
stdio: 'ignore', stdio: 'ignore'
shell: process.platform === 'win32',
}); });
return true; return true;
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
return false; return false;
} }
throw error;
}
} }
async function installVercelCLI() { async function installVercelCLI(): Promise<void> {
console.log('Installing Vercel CLI...'); console.log('Installing Vercel CLI...');
execSync('npm install -g vercel', { execSync('npm install -g vercel', {
stdio: 'inherit', stdio: 'inherit'
shell: process.platform === 'win32',
}); });
} }
async function getVercelToken() { async function getVercelToken(): Promise<string | null> {
try { try {
// Try to get token from Vercel CLI config // Try to get token from Vercel CLI config
const configPath = path.join(os.homedir(), '.vercel', 'auth.json'); const configPath = path.join(os.homedir(), '.vercel', 'auth.json');
@ -221,9 +198,11 @@ async function getVercelToken() {
const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8')); const authConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return authConfig.token; return authConfig.token;
} }
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
console.warn('Could not read Vercel token from config file'); console.warn('Could not read Vercel token from config file');
} }
}
// Try environment variable // Try environment variable
if (process.env.VERCEL_TOKEN) { if (process.env.VERCEL_TOKEN) {
@ -241,14 +220,15 @@ async function getVercelToken() {
// The token isn't directly exposed, so we'll need to use CLI for some operations // The token isn't directly exposed, so we'll need to use CLI for some operations
console.log('✅ Verified Vercel CLI authentication'); console.log('✅ Verified Vercel CLI authentication');
return null; // We'll fall back to CLI operations return null; // We'll fall back to CLI operations
} catch (error) { } catch (error: unknown) {
throw new Error( if (error instanceof Error) {
'Not logged in to Vercel CLI. Please run this script again to login.', throw new Error('Not logged in to Vercel CLI. Please run this script again to login.');
); }
throw error;
} }
} }
async function loginToVercel() { async function loginToVercel(): Promise<boolean> {
console.log('\n🔑 Vercel Login'); console.log('\n🔑 Vercel Login');
console.log('You can either:'); console.log('You can either:');
console.log('1. Log in to an existing Vercel account'); 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('3. Complete the Vercel account setup in your browser');
console.log('4. Return here once your Vercel account is created\n'); console.log('4. Return here once your Vercel account is created\n');
console.log( 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'], { const child = spawn('vercel', ['login'], {
stdio: 'inherit', stdio: 'inherit',
}); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
child.on('close', code => { child.on('close', (code) => {
resolve(); resolve();
}); });
}); });
console.log('\n📱 Waiting for login to complete...'); console.log('\n📱 Waiting for login to complete...');
console.log( 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++) { for (let i = 0; i < 150; i++) {
@ -282,11 +262,11 @@ async function loginToVercel() {
execSync('vercel whoami', { stdio: 'ignore' }); execSync('vercel whoami', { stdio: 'ignore' });
console.log('✅ Successfully logged in to Vercel!'); console.log('✅ Successfully logged in to Vercel!');
return true; return true;
} catch (error) { } catch (error: unknown) {
if (error.message.includes('Account not found')) { if (error instanceof Error && error.message.includes('Account not found')) {
console.log(' Waiting for Vercel account setup to complete...'); 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; return false;
} }
async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { async function setVercelEnvVarSDK(vercelClient: Vercel, projectId: string, key: string, value: string | object): Promise<boolean> {
try { try {
let processedValue; let processedValue: string;
if (typeof value === 'object') { if (typeof value === 'object') {
processedValue = JSON.stringify(value); processedValue = JSON.stringify(value);
} else { } else {
@ -311,8 +291,8 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) {
idOrName: projectId, idOrName: projectId,
}); });
const existingVar = existingVars.envs?.find( const existingVar = existingVars.envs?.find((env: any) =>
env => env.key === key && env.target?.includes('production'), env.key === key && env.target?.includes('production')
); );
if (existingVar) { if (existingVar) {
@ -341,16 +321,16 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) {
} }
return true; return true;
} catch (error) { } catch (error: unknown) {
console.warn( if (error instanceof Error) {
`⚠️ Warning: Failed to set environment variable ${key}:`, console.warn(`⚠️ Warning: Failed to set environment variable ${key}:`, error.message);
error.message,
);
return false; return false;
} }
throw error;
}
} }
async function setVercelEnvVarCLI(key, value, projectRoot) { async function setVercelEnvVarCLI(key: string, value: string | object, projectRoot: string): Promise<boolean> {
try { try {
// Remove existing env var // Remove existing env var
try { try {
@ -359,11 +339,11 @@ async function setVercelEnvVarCLI(key, value, projectRoot) {
stdio: 'ignore', stdio: 'ignore',
env: process.env, env: process.env,
}); });
} catch (error) { } catch (error: unknown) {
// Ignore errors from removal // Ignore errors from removal
} }
let processedValue; let processedValue: string;
if (typeof value === 'object') { if (typeof value === 'object') {
processedValue = JSON.stringify(value); processedValue = JSON.stringify(value);
} else { } else {
@ -375,7 +355,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) {
fs.writeFileSync(tempFilePath, processedValue, 'utf8'); fs.writeFileSync(tempFilePath, processedValue, 'utf8');
// Use appropriate command based on platform // Use appropriate command based on platform
let command; let command: string;
if (process.platform === 'win32') { if (process.platform === 'win32') {
command = `type "${tempFilePath}" | vercel env add ${key} production`; command = `type "${tempFilePath}" | vercel env add ${key} production`;
} else { } else {
@ -385,35 +365,29 @@ async function setVercelEnvVarCLI(key, value, projectRoot) {
execSync(command, { execSync(command, {
cwd: projectRoot, cwd: projectRoot,
stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts stdio: 'pipe', // Changed from 'inherit' to avoid interactive prompts
shell: true, env: process.env
env: process.env,
}); });
fs.unlinkSync(tempFilePath); fs.unlinkSync(tempFilePath);
console.log(`✅ Set environment variable: ${key}`); console.log(`✅ Set environment variable: ${key}`);
return true; return true;
} catch (error) { } catch (error: unknown) {
const tempFilePath = path.join(projectRoot, `${key}_temp.txt`); const tempFilePath = path.join(projectRoot, `${key}_temp.txt`);
if (fs.existsSync(tempFilePath)) { if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath); fs.unlinkSync(tempFilePath);
} }
console.warn( if (error instanceof Error) {
`⚠️ Warning: Failed to set environment variable ${key}:`, console.warn(`⚠️ Warning: Failed to set environment variable ${key}:`, error.message);
error.message,
);
return false; return false;
} }
throw error;
}
} }
async function setEnvironmentVariables( async function setEnvironmentVariables(vercelClient: Vercel | null, projectId: string | null, envVars: Record<string, string | object>, projectRoot: string): Promise<Array<{ key: string; success: boolean }>> {
vercelClient,
projectId,
envVars,
projectRoot,
) {
console.log('\n📝 Setting up environment variables...'); console.log('\n📝 Setting up environment variables...');
const results = []; const results: Array<{ key: string; success: boolean }> = [];
for (const [key, value] of Object.entries(envVars)) { for (const [key, value] of Object.entries(envVars)) {
if (!value) continue; if (!value) continue;
@ -434,35 +408,30 @@ async function setEnvironmentVariables(
} }
// Report results // Report results
const failed = results.filter(r => !r.success); const failed = results.filter((r) => !r.success);
if (failed.length > 0) { if (failed.length > 0) {
console.warn(`\n⚠ Failed to set ${failed.length} environment variables:`); 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( 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; return results;
} }
async function waitForDeployment( async function waitForDeployment(vercelClient: Vercel | null, projectId: string, maxWaitTime = 300000): Promise<any> { // 5 minutes
vercelClient,
projectId,
maxWaitTime = 300000,
) {
// 5 minutes
console.log('\n⏳ Waiting for deployment to complete...'); console.log('\n⏳ Waiting for deployment to complete...');
const startTime = Date.now(); const startTime = Date.now();
while (Date.now() - startTime < maxWaitTime) { while (Date.now() - startTime < maxWaitTime) {
try { try {
const deployments = await vercelClient.deployments.list({ const deployments = await vercelClient?.deployments.list({
projectId: projectId, projectId: projectId,
limit: 1, limit: 1,
}); });
if (deployments.deployments?.[0]) { if (deployments?.deployments?.[0]) {
const deployment = deployments.deployments[0]; const deployment = deployments.deployments[0];
console.log(`📊 Deployment status: ${deployment.state}`); console.log(`📊 Deployment status: ${deployment.state}`);
@ -476,21 +445,24 @@ async function waitForDeployment(
} }
// Still building, wait and check again // 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 { } else {
console.log('⏳ No deployment found yet, waiting...'); console.log('⏳ No deployment found yet, waiting...');
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise((resolve) => setTimeout(resolve, 5000));
} }
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
console.warn('⚠️ Could not check deployment status:', error.message); console.warn('⚠️ Could not check deployment status:', error.message);
await new Promise(resolve => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
} }
throw error;
}
} }
throw new Error('Deployment timed out after 5 minutes'); throw new Error('Deployment timed out after 5 minutes');
} }
async function deployToVercel(useGitHub = false) { async function deployToVercel(useGitHub = false): Promise<void> {
try { try {
console.log('\n🚀 Deploying to Vercel...'); console.log('\n🚀 Deploying to Vercel...');
@ -506,18 +478,18 @@ async function deployToVercel(useGitHub = false) {
framework: 'nextjs', framework: 'nextjs',
}, },
null, null,
2, 2
), )
); );
} }
// Set up Vercel project // Set up Vercel project
console.log('\n📦 Setting up Vercel project...'); console.log('\n📦 Setting up Vercel project...');
console.log( 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( 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 // Use spawn instead of execSync for better error handling
@ -525,11 +497,11 @@ async function deployToVercel(useGitHub = false) {
const vercelSetup = spawn('vercel', [], { const vercelSetup = spawn('vercel', [], {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit', stdio: 'inherit',
shell: process.platform === 'win32', shell: process.platform === 'win32' ? true : undefined
}); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelSetup.on('close', code => { vercelSetup.on('close', (code) => {
if (code === 0 || code === null) { if (code === 0 || code === null) {
console.log('✅ Vercel project setup completed'); console.log('✅ Vercel project setup completed');
resolve(); resolve();
@ -539,48 +511,50 @@ async function deployToVercel(useGitHub = false) {
} }
}); });
vercelSetup.on('error', error => { vercelSetup.on('error', (error) => {
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 resolve(); // Don't reject, as this is often expected
}); });
}); });
// Wait a moment for project files to be written // 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 // Load project info
let projectId; let projectId: string;
try { try {
const projectJson = JSON.parse( const projectJson = JSON.parse(
fs.readFileSync('.vercel/project.json', 'utf8'), fs.readFileSync('.vercel/project.json', 'utf8')
); );
projectId = projectJson.projectId; projectId = projectJson.projectId;
} catch (error) { } catch (error: unknown) {
throw new Error( if (error instanceof Error) {
'Failed to load project info. Please ensure the Vercel project was created successfully.', throw new Error('Failed to load project info. Please ensure the Vercel project was created successfully.');
); }
throw error;
} }
// Get Vercel token and initialize SDK client // Get Vercel token and initialize SDK client
let vercelClient = null; let vercelClient: Vercel | null = null;
try { try {
const token = await getVercelToken(); const token = await getVercelToken();
if (token) { if (token) {
vercelClient = new Vercel({ vercelClient = new Vercel({
bearerToken: token, bearerToken: token
}); });
console.log('✅ Initialized Vercel SDK client'); console.log('✅ Initialized Vercel SDK client');
} }
} catch (error) { } catch (error: unknown) {
console.warn( if (error instanceof Error) {
'⚠️ Could not initialize Vercel SDK, falling back to CLI operations', console.warn('⚠️ Could not initialize Vercel SDK, falling back to CLI operations');
); }
throw error;
} }
// Get project details // Get project details
console.log('\n🔍 Getting project details...'); console.log('\n🔍 Getting project details...');
let domain; let domain: string | undefined;
let projectName; let projectName: string | undefined;
if (vercelClient) { if (vercelClient) {
try { try {
@ -590,10 +564,11 @@ async function deployToVercel(useGitHub = false) {
projectName = project.name; projectName = project.name;
domain = `${projectName}.vercel.app`; domain = `${projectName}.vercel.app`;
console.log('🌐 Using project name for domain:', domain); console.log('🌐 Using project name for domain:', domain);
} catch (error) { } catch (error: unknown) {
console.warn( if (error instanceof Error) {
'⚠️ Could not get project details via SDK, using CLI fallback', 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, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
}, }
); );
const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/);
@ -621,31 +596,23 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Using project name for domain:', domain); console.log('🌐 Using project name for domain:', domain);
} else { } else {
console.warn( 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 // Use a fallback domain based on project ID
domain = `project-${projectId.slice(-8)}.vercel.app`; domain = `project-${projectId.slice(-8)}.vercel.app`;
console.log('🌐 Using fallback domain:', domain); console.log('🌐 Using fallback domain:', domain);
} }
} }
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
console.warn('⚠️ Could not inspect project, using fallback domain'); console.warn('⚠️ Could not inspect project, using fallback domain');
// Use a fallback domain based on project ID // Use a fallback domain based on project ID
domain = `project-${projectId.slice(-8)}.vercel.app`; domain = `project-${projectId.slice(-8)}.vercel.app`;
console.log('🌐 Using fallback domain:', domain); 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 // Prepare environment variables
const nextAuthSecret = const nextAuthSecret =
@ -656,21 +623,14 @@ async function deployToVercel(useGitHub = false) {
NEXTAUTH_URL: `https://${domain}`, NEXTAUTH_URL: `https://${domain}`,
NEXT_PUBLIC_URL: `https://${domain}`, NEXT_PUBLIC_URL: `https://${domain}`,
...(process.env.NEYNAR_API_KEY && { ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: 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 }),
...(process.env.NEYNAR_CLIENT_ID && {
NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID,
}),
...(process.env.SPONSOR_SIGNER && {
SPONSOR_SIGNER: process.env.SPONSOR_SIGNER,
}),
...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }),
...Object.fromEntries( ...Object.fromEntries(
Object.entries(process.env).filter(([key]) => Object.entries(process.env).filter(([key]) =>
key.startsWith('NEXT_PUBLIC_'), key.startsWith('NEXT_PUBLIC_')
), )
), ),
}; };
@ -679,7 +639,7 @@ async function deployToVercel(useGitHub = false) {
vercelClient, vercelClient,
projectId, projectId,
vercelEnv, vercelEnv,
projectRoot, projectRoot
); );
// Deploy the project // Deploy the project
@ -702,8 +662,8 @@ async function deployToVercel(useGitHub = false) {
env: process.env, env: process.env,
}); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelDeploy.on('close', code => { vercelDeploy.on('close', (code) => {
if (code === 0) { if (code === 0) {
console.log('✅ Vercel deployment command completed'); console.log('✅ Vercel deployment command completed');
resolve(); resolve();
@ -713,24 +673,24 @@ async function deployToVercel(useGitHub = false) {
} }
}); });
vercelDeploy.on('error', error => { vercelDeploy.on('error', (error) => {
console.error('❌ Vercel deployment error:', error.message); console.error('❌ Vercel deployment error:', error.message);
reject(error); reject(error);
}); });
}); });
// Wait for deployment to actually complete // Wait for deployment to actually complete
let deployment; let deployment: any;
if (vercelClient) { if (vercelClient) {
try { try {
deployment = await waitForDeployment(vercelClient, projectId); deployment = await waitForDeployment(vercelClient, projectId);
} catch (error) { } catch (error: unknown) {
console.warn( if (error instanceof Error) {
'⚠️ Could not verify deployment completion:', console.warn('⚠️ Could not verify deployment completion:', error.message);
error.message,
);
console.log(' Proceeding with domain verification...'); console.log(' Proceeding with domain verification...');
} }
throw error;
}
} }
// Verify actual domain after deployment // Verify actual domain after deployment
@ -741,10 +701,11 @@ async function deployToVercel(useGitHub = false) {
try { try {
actualDomain = deployment.url || domain; actualDomain = deployment.url || domain;
console.log('🌐 Verified actual domain:', actualDomain); console.log('🌐 Verified actual domain:', actualDomain);
} catch (error) { } catch (error: unknown) {
console.warn( if (error instanceof Error) {
'⚠️ Could not verify domain via SDK, using assumed domain', 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) { if (actualDomain !== domain) {
console.log('🔄 Updating environment variables with correct domain...'); console.log('🔄 Updating environment variables with correct domain...');
const webhookUrl = const updatedEnv: Record<string, string | object> = {
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `https://${actualDomain}/api/webhook`;
const updatedEnv = {
NEXTAUTH_URL: `https://${actualDomain}`, NEXTAUTH_URL: `https://${actualDomain}`,
NEXT_PUBLIC_URL: `https://${actualDomain}`, NEXT_PUBLIC_URL: `https://${actualDomain}`,
}; };
if (miniAppMetadata) { await setEnvironmentVariables(vercelClient, projectId, updatedEnv, projectRoot);
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,
);
console.log('\n📦 Redeploying with correct domain...'); console.log('\n📦 Redeploying with correct domain...');
const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], { const vercelRedeploy = spawn('vercel', ['deploy', '--prod'], {
@ -787,8 +727,8 @@ async function deployToVercel(useGitHub = false) {
env: process.env, env: process.env,
}); });
await new Promise((resolve, reject) => { await new Promise<void>((resolve, reject) => {
vercelRedeploy.on('close', code => { vercelRedeploy.on('close', (code) => {
if (code === 0) { if (code === 0) {
console.log('✅ Redeployment completed'); console.log('✅ Redeployment completed');
resolve(); resolve();
@ -798,7 +738,7 @@ async function deployToVercel(useGitHub = false) {
} }
}); });
vercelRedeploy.on('error', error => { vercelRedeploy.on('error', (error) => {
console.error('❌ Redeployment error:', error.message); console.error('❌ Redeployment error:', error.message);
reject(error); 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('\n✨ Deployment complete! Your mini app is now live at:');
console.log(`🌐 https://${domain}`); console.log(`🌐 https://${domain}`);
console.log( console.log('\n📝 You can manage your project at https://vercel.com/dashboard');
'\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) { 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); console.error('\n❌ Deployment failed:', error.message);
process.exit(1); process.exit(1);
} }
throw error;
}
} }
async function main() { async function main(): Promise<void> {
try { try {
console.log('🚀 Vercel Mini App Deployment (SDK Edition)'); console.log('🚀 Vercel Mini App Deployment (SDK Edition)');
console.log( 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('\nThe script will:');
console.log('1. Check for required environment variables'); console.log('1. Check for required environment variables');
@ -833,14 +815,17 @@ async function main() {
// Check if @vercel/sdk is installed // Check if @vercel/sdk is installed
try { try {
await import('@vercel/sdk'); await import('@vercel/sdk');
} catch (error) { } catch (error: unknown) {
if (error instanceof Error) {
console.log('📦 Installing @vercel/sdk...'); console.log('📦 Installing @vercel/sdk...');
execSync('npm install @vercel/sdk', { execSync('npm install @vercel/sdk', {
cwd: projectRoot, cwd: projectRoot,
stdio: 'inherit', stdio: 'inherit'
}); });
console.log('✅ @vercel/sdk installed successfully'); console.log('✅ @vercel/sdk installed successfully');
} }
throw error;
}
await checkRequiredEnvVars(); await checkRequiredEnvVars();
@ -895,10 +880,14 @@ async function main() {
} }
await deployToVercel(useGitHub); await deployToVercel(useGitHub);
} catch (error) {
} catch (error: unknown) {
if (error instanceof Error) {
console.error('\n❌ Error:', error.message); console.error('\n❌ Error:', error.message);
process.exit(1); process.exit(1);
} }
throw error;
}
} }
main(); main();

View File

@ -1,9 +1,9 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getFarcasterMetadata } from '../../../lib/utils'; import { getFarcasterDomainManifest } from '~/lib/utils';
export async function GET() { export async function GET() {
try { try {
const config = await getFarcasterMetadata(); const config = await getFarcasterDomainManifest();
return NextResponse.json(config); return NextResponse.json(config);
} catch (error) { } catch (error) {
console.error('Error generating metadata:', error); console.error('Error generating metadata:', error);

View File

@ -118,7 +118,7 @@ export function AuthDialog({
const content = getStepContent(); const content = getStepContent();
return ( return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"> <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4">
<div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-md shadow-2xl border border-gray-200 dark:border-gray-700 max-h-[80vh] sm:max-h-[90vh] flex flex-col"> <div className="bg-white dark:bg-gray-800 rounded-xl w-full max-w-md shadow-2xl border border-gray-200 dark:border-gray-700 max-h-[80vh] sm:max-h-[90vh] flex flex-col">
<div className="flex justify-between items-center p-4 sm:p-6 pb-3 sm:pb-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0"> <div className="flex justify-between items-center p-4 sm:p-6 pb-3 sm:pb-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100"> <h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">

View File

@ -1,9 +1,10 @@
'use client'; 'use client';
import { useCallback, useState, useEffect } from 'react'; import { useCallback, useState, useEffect } from 'react';
import { type ComposeCast } from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react';
import { Button } from './Button'; import { Button } from './Button';
import { useMiniApp } from '@neynar/react';
import { type ComposeCast } from '@farcaster/miniapp-sdk';
import { APP_URL } from '~/lib/constants';
interface EmbedConfig { interface EmbedConfig {
path?: string; path?: string;
@ -79,8 +80,7 @@ export function ShareButton({
return embed; return embed;
} }
if (embed.path) { if (embed.path) {
const baseUrl = const baseUrl = APP_URL || window.location.origin;
process.env.NEXT_PUBLIC_URL || window.location.origin;
const url = new URL(`${baseUrl}${embed.path}`); const url = new URL(`${baseUrl}${embed.path}`);
// Add UTM parameters // Add UTM parameters

View File

@ -3,6 +3,7 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import { type Haptics } from '@farcaster/miniapp-sdk'; import { type Haptics } from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react'; import { useMiniApp } from '@neynar/react';
import { APP_URL } from '~/lib/constants';
import { Button } from '../Button'; import { Button } from '../Button';
import { NeynarAuthButton } from '../NeynarAuthButton/index'; import { NeynarAuthButton } from '../NeynarAuthButton/index';
import { ShareButton } from '../Share'; import { ShareButton } from '../Share';
@ -96,7 +97,7 @@ export function ActionsTab() {
*/ */
const copyUserShareUrl = useCallback(async () => { const copyUserShareUrl = useCallback(async () => {
if (context?.user?.fid) { 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); await navigator.clipboard.writeText(userShareUrl);
setNotificationState(prev => ({ ...prev, shareUrlCopied: true })); setNotificationState(prev => ({ ...prev, shareUrlCopied: true }));
setTimeout( setTimeout(
@ -130,9 +131,7 @@ export function ActionsTab() {
cast={{ cast={{
text: 'Check out this awesome frame @1 @2 @3! 🚀🪐', text: 'Check out this awesome frame @1 @2 @3! 🚀🪐',
bestFriends: true, bestFriends: true,
embeds: [ embeds: [`${APP_URL}/share/${context?.user?.fid || ''}`],
`${process.env.NEXT_PUBLIC_URL}/share/${context?.user?.fid || ''}`,
],
}} }}
className="w-full" className="w-full"
/> />

View File

@ -1,3 +1,5 @@
import { type AccountAssociation } from '@farcaster/miniapp-node';
/** /**
* Application constants and configuration values. * Application constants and configuration values.
* *
@ -14,63 +16,70 @@
* The base URL of the application. * The base URL of the application.
* Used for generating absolute URLs for assets and API endpoints. * 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. * The name of the mini app as displayed to users.
* Used in titles, headers, and app store listings. * 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. * A brief description of the mini app's functionality.
* Used in app store listings and metadata. * 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. * The primary category for the mini app.
* Used for app store categorization and discovery. * 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. * Tags associated with the mini app.
* Used for search and discovery in app stores. * 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 --- // --- Asset URLs ---
/** /**
* URL for the app's icon image. * URL for the app's icon image.
* Used in app store listings and UI elements. * 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. * URL for the app's Open Graph image.
* Used for social media sharing and previews. * 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. * URL for the app's splash screen image.
* Displayed during app loading. * 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. * Background color for the splash screen.
* Used as fallback when splash image is loading. * 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 --- // --- UI Configuration ---
/** /**
* Text displayed on the main action button. * Text displayed on the main action button.
* Used for the primary call-to-action in the mini app. * 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 --- // --- Integration Configuration ---
/** /**
@ -80,8 +89,7 @@ export const APP_BUTTON_TEXT = 'Launch Mini App';
* Neynar webhook endpoint. Otherwise, falls back to a local webhook * Neynar webhook endpoint. Otherwise, falls back to a local webhook
* endpoint for development and testing. * endpoint for development and testing.
*/ */
export const APP_WEBHOOK_URL = export const APP_WEBHOOK_URL: string = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `${APP_URL}/api/webhook`; : `${APP_URL}/api/webhook`;
@ -92,7 +100,7 @@ export const APP_WEBHOOK_URL =
* When false, wallet functionality is completely hidden from the UI. * When false, wallet functionality is completely hidden from the UI.
* Useful for mini apps that don't require wallet integration. * 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. * Flag to enable/disable analytics tracking.
@ -101,7 +109,7 @@ export const USE_WALLET = true;
* When false, analytics collection is disabled. * When false, analytics collection is disabled.
* Useful for privacy-conscious users or development environments. * Useful for privacy-conscious users or development environments.
*/ */
export const ANALYTICS_ENABLED = true; export const ANALYTICS_ENABLED: boolean = true;
// PLEASE DO NOT UPDATE THIS // PLEASE DO NOT UPDATE THIS
export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = { export const SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN = {

View File

@ -1,5 +1,6 @@
import { type ClassValue, clsx } from 'clsx'; import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
import { type Manifest } from '@farcaster/miniapp-node';
import { import {
APP_BUTTON_TEXT, APP_BUTTON_TEXT,
APP_DESCRIPTION, APP_DESCRIPTION,
@ -8,35 +9,12 @@ import {
APP_OG_IMAGE_URL, APP_OG_IMAGE_URL,
APP_PRIMARY_CATEGORY, APP_PRIMARY_CATEGORY,
APP_SPLASH_BACKGROUND_COLOR, APP_SPLASH_BACKGROUND_COLOR,
APP_SPLASH_URL,
APP_TAGS, APP_TAGS,
APP_URL, APP_URL,
APP_WEBHOOK_URL, APP_WEBHOOK_URL,
APP_ACCOUNT_ASSOCIATION,
} from './constants'; } 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[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)); return twMerge(clsx(inputs));
@ -63,31 +41,10 @@ export function getMiniAppEmbedMetadata(ogImageUrl?: string) {
}; };
} }
export async function getFarcasterMetadata(): Promise<MiniAppManifest> { export async function getFarcasterDomainManifest(): Promise<Manifest> {
// 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');
}
return { return {
accountAssociation: { accountAssociation: APP_ACCOUNT_ASSOCIATION,
header: '', miniapp: {
payload: '',
signature: '',
},
frame: {
version: '1', version: '1',
name: APP_NAME ?? 'Neynar Starter Kit', name: APP_NAME ?? 'Neynar Starter Kit',
iconUrl: APP_ICON_URL, iconUrl: APP_ICON_URL,