mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
feat: update to query neynar client id from neynar
This commit is contained in:
parent
1cc837ca86
commit
b18ff0da95
190
bin/index.js
190
bin/index.js
@ -36,6 +36,28 @@ Let's create your Frame! 🚀
|
|||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
console.log('Neynar app data:', data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error querying Neynar app data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function lookupFidByCustodyAddress(custodyAddress, apiKey) {
|
async function lookupFidByCustodyAddress(custodyAddress, apiKey) {
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
throw new Error('Neynar API key is required');
|
throw new Error('Neynar API key is required');
|
||||||
@ -66,11 +88,101 @@ async function lookupFidByCustodyAddress(custodyAddress, apiKey) {
|
|||||||
async function init() {
|
async function init() {
|
||||||
printWelcomeMessage();
|
printWelcomeMessage();
|
||||||
|
|
||||||
|
// Ask about Neynar usage
|
||||||
|
let useNeynar = true;
|
||||||
|
let neynarApiKey = null;
|
||||||
|
let neynarClientId = null;
|
||||||
|
let neynarAppName = null;
|
||||||
|
let neynarAppLogoUrl = null;
|
||||||
|
|
||||||
|
while (useNeynar) {
|
||||||
|
const neynarAnswers = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'useNeynar',
|
||||||
|
message: '🪐 Neynar is an API that makes it easy to build on Farcaster.\n\nBenefits of using Neynar in your frame:\n- Pre-configured webhook handling (no setup required)\n- Automatic frame analytics in your dev portal\n- Send manual notifications from dev.neynar.com\n- Built-in rate limiting and error handling\n\nWould you like to use Neynar in your frame?',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!neynarAnswers.useNeynar) {
|
||||||
|
useNeynar = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const neynarKeyAnswer = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'password',
|
||||||
|
name: 'neynarApiKey',
|
||||||
|
message: 'Enter your Neynar API key (or press enter to skip):',
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (neynarKeyAnswer.neynarApiKey) {
|
||||||
|
neynarApiKey = neynarKeyAnswer.neynarApiKey;
|
||||||
|
} else {
|
||||||
|
const useDemoKey = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'useDemo',
|
||||||
|
message: 'Would you like to try the demo Neynar API key?',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
neynarApiKey = useDemoKey.useDemo ? 'FARCASTER_V2_FRAMES_DEMO' : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!neynarApiKey) {
|
||||||
|
console.log('\n⚠️ No valid API key provided. Would you like to try again?');
|
||||||
|
const { retry } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'retry',
|
||||||
|
message: 'Try configuring Neynar again?',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
if (!retry) {
|
||||||
|
useNeynar = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appInfo = await queryNeynarApp(neynarApiKey);
|
||||||
|
if (appInfo) {
|
||||||
|
neynarClientId = appInfo.app_uuid;
|
||||||
|
neynarAppName = appInfo.app_name;
|
||||||
|
neynarAppLogoUrl = appInfo.logo_url;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!neynarClientId) {
|
||||||
|
const { retry } = await inquirer.prompt([
|
||||||
|
{
|
||||||
|
type: 'confirm',
|
||||||
|
name: 'retry',
|
||||||
|
message: '⚠️ Could not find a client ID for this API key. Would you like to try configuring Neynar again?',
|
||||||
|
default: true
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
if (!retry) {
|
||||||
|
useNeynar = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we have both API key and client ID
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
const answers = await inquirer.prompt([
|
const answers = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'projectName',
|
name: 'projectName',
|
||||||
message: 'What is the name of your frame?',
|
message: 'What is the name of your frame?',
|
||||||
|
default: neynarAppName || undefined,
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
if (input.trim() === '') {
|
if (input.trim() === '') {
|
||||||
return 'Project name cannot be empty';
|
return 'Project name cannot be empty';
|
||||||
@ -105,76 +217,16 @@ async function init() {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'splashImageUrl',
|
name: 'splashImageUrl',
|
||||||
message: 'Enter the URL for your splash image\n(optional -- leave blank to use the default public/splash.png image or replace public/splash.png with your own)\n\nExternal splash image URL:',
|
message: 'Enter the URL for your splash image\n(optional -- leave blank to use the default public/splash.png image or replace public/splash.png with your own)\n\nExternal splash image URL:',
|
||||||
default: null
|
default: neynarAppLogoUrl || undefined
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'iconImageUrl',
|
name: 'iconImageUrl',
|
||||||
message: 'Enter the URL for your app icon\n(optional -- leave blank to use the default public/icon.png image or replace public/icon.png with your own)\n\nExternal app icon URL:',
|
message: 'Enter the URL for your app icon\n(optional -- leave blank to use the default public/icon.png image or replace public/icon.png with your own)\n\nExternal app icon URL:',
|
||||||
default: null
|
default: neynarAppLogoUrl || undefined
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Handle Neynar API key
|
|
||||||
const neynarFlow = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'useNeynar',
|
|
||||||
message: '🪐 Neynar is an API that makes it easy to build on Farcaster.\n\nBenefits of using Neynar in your frame:\n- Pre-configured webhook handling (no setup required)\n- Automatic frame analytics in your dev portal\n- Send manual notifications from dev.neynar.com\n- Built-in rate limiting and error handling\n\nWould you like to use Neynar in your frame?',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (neynarFlow.useNeynar) {
|
|
||||||
const neynarKeyAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'password',
|
|
||||||
name: 'neynarApiKey',
|
|
||||||
message: 'Enter your Neynar API key (or press enter to skip):',
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (!neynarKeyAnswer.neynarApiKey) {
|
|
||||||
const useDemoKey = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'confirm',
|
|
||||||
name: 'useDemo',
|
|
||||||
message: 'Would you like to try the demo Neynar API key?',
|
|
||||||
default: true
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
|
|
||||||
answers.useNeynar = useDemoKey.useDemo;
|
|
||||||
answers.neynarApiKey = useDemoKey.useDemo ? 'FARCASTER_V2_FRAMES_DEMO' : null;
|
|
||||||
} else {
|
|
||||||
answers.useNeynar = true;
|
|
||||||
answers.neynarApiKey = neynarKeyAnswer.neynarApiKey;
|
|
||||||
|
|
||||||
// Get Neynar client ID if using Neynar
|
|
||||||
if (answers.useNeynar) {
|
|
||||||
const neynarClientIdAnswer = await inquirer.prompt([
|
|
||||||
{
|
|
||||||
type: 'input',
|
|
||||||
name: 'neynarClientId',
|
|
||||||
message: 'Enter your Neynar client ID:',
|
|
||||||
validate: (input) => {
|
|
||||||
if (input && !/^[a-zA-Z0-9-]+$/.test(input)) {
|
|
||||||
return 'Invalid Neynar client ID format';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
answers.neynarClientId = neynarClientIdAnswer.neynarClientId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
answers.useNeynar = false;
|
|
||||||
answers.neynarApiKey = null;
|
|
||||||
answers.neynarClientId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ask about localhost vs tunnel
|
// Ask about localhost vs tunnel
|
||||||
const hostingAnswer = await inquirer.prompt([
|
const hostingAnswer = await inquirer.prompt([
|
||||||
{
|
{
|
||||||
@ -221,8 +273,7 @@ async function init() {
|
|||||||
|
|
||||||
// Look up FID using custody address
|
// Look up FID using custody address
|
||||||
console.log('\nUsing seed phrase to look up FID by custody address...');
|
console.log('\nUsing seed phrase to look up FID by custody address...');
|
||||||
const neynarApiKey = answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO';
|
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO');
|
||||||
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey);
|
|
||||||
|
|
||||||
if (!fid) {
|
if (!fid) {
|
||||||
throw new Error('No FID found for this custody address');
|
throw new Error('No FID found for this custody address');
|
||||||
@ -339,7 +390,7 @@ async function init() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Add Neynar dependencies if selected
|
// Add Neynar dependencies if selected
|
||||||
if (answers.useNeynar) {
|
if (useNeynar) {
|
||||||
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
|
packageJson.dependencies['@neynar/nodejs-sdk'] = '^2.19.0';
|
||||||
packageJson.dependencies['@neynar/react'] = '^0.9.7';
|
packageJson.dependencies['@neynar/react'] = '^0.9.7';
|
||||||
}
|
}
|
||||||
@ -364,8 +415,7 @@ async function init() {
|
|||||||
// Look up FID using custody address
|
// Look up FID using custody address
|
||||||
if (!fid) {
|
if (!fid) {
|
||||||
console.log('\nLooking up FID...');
|
console.log('\nLooking up FID...');
|
||||||
const neynarApiKey = answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO';
|
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey ?? 'FARCASTER_V2_FRAMES_DEMO');
|
||||||
fid = await lookupFidByCustodyAddress(custodyAddress, neynarApiKey);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write seed phrase and FID to .env.local for manifest signature generation
|
// Write seed phrase and FID to .env.local for manifest signature generation
|
||||||
@ -385,9 +435,9 @@ async function init() {
|
|||||||
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`);
|
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`);
|
||||||
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`);
|
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`);
|
||||||
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`);
|
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`);
|
||||||
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${answers.useNeynar ? answers.neynarApiKey : 'FARCASTER_V2_FRAMES_DEMO'}"`);
|
if (useNeynar) {
|
||||||
if (answers.neynarClientId) {
|
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
|
||||||
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${answers.neynarClientId}"`);
|
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
|
||||||
}
|
}
|
||||||
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
fs.appendFileSync(envPath, `\nUSE_TUNNEL="${answers.useTunnel}"`);
|
||||||
|
|
||||||
|
|||||||
107
scripts/build.js
107
scripts/build.js
@ -28,11 +28,32 @@ async function validateDomain(domain) {
|
|||||||
return cleanDomain;
|
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();
|
||||||
|
console.log('Neynar app data:', data);
|
||||||
|
return data;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error querying Neynar app data:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function validateSeedPhrase(seedPhrase) {
|
async function validateSeedPhrase(seedPhrase) {
|
||||||
try {
|
try {
|
||||||
// Try to create an account from the seed phrase
|
// Try to create an account from the seed phrase
|
||||||
const account = mnemonicToAccount(seedPhrase);
|
const account = mnemonicToAccount(seedPhrase);
|
||||||
console.log('✅ Seed phrase validated successfully');
|
|
||||||
return account.address;
|
return account.address;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new Error('Invalid seed phrase');
|
throw new Error('Invalid seed phrase');
|
||||||
@ -102,7 +123,7 @@ async function main() {
|
|||||||
type: 'input',
|
type: 'input',
|
||||||
name: 'frameName',
|
name: 'frameName',
|
||||||
message: 'Enter the name for your frame (e.g., My Cool Frame):',
|
message: 'Enter the name for your frame (e.g., My Cool Frame):',
|
||||||
default: process.env.NEXT_PUBLIC_FRAME_NAME || 'Frames v2 Demo',
|
default: process.env.NEXT_PUBLIC_FRAME_NAME,
|
||||||
validate: (input) => {
|
validate: (input) => {
|
||||||
if (input.trim() === '') {
|
if (input.trim() === '') {
|
||||||
return 'Frame name cannot be empty';
|
return 'Frame name cannot be empty';
|
||||||
@ -128,47 +149,57 @@ async function main() {
|
|||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Get Neynar API key from user if not already in .env.local
|
// Get Neynar configuration
|
||||||
let neynarApiKey = process.env.NEYNAR_API_KEY;
|
let neynarApiKey = process.env.NEYNAR_API_KEY;
|
||||||
let neynarClientId = null;
|
let neynarClientId = process.env.NEYNAR_CLIENT_ID;
|
||||||
|
let useNeynar = true;
|
||||||
|
|
||||||
if (!neynarApiKey) {
|
while (useNeynar) {
|
||||||
const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([
|
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')
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only ask for client ID if we have an API key
|
|
||||||
if (neynarApiKey) {
|
|
||||||
neynarClientId = process.env.NEYNAR_CLIENT_ID;
|
|
||||||
if (!neynarClientId) {
|
|
||||||
const { neynarClientId: inputNeynarClientId } = await inquirer.prompt([
|
|
||||||
{
|
{
|
||||||
type: 'input',
|
type: 'password',
|
||||||
name: 'neynarClientId',
|
name: 'neynarApiKey',
|
||||||
message: 'Enter your Neynar client ID (required for Neynar webhook):',
|
message: 'Enter your Neynar API key (optional - leave blank to skip):',
|
||||||
validate: (input) => {
|
default: null
|
||||||
if (!input) {
|
|
||||||
return 'Client ID is required when using Neynar API key';
|
|
||||||
}
|
|
||||||
if (!/^[a-zA-Z0-9-]+$/.test(input)) {
|
|
||||||
return 'Invalid Neynar client ID format';
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
]);
|
]);
|
||||||
neynarClientId = inputNeynarClientId;
|
neynarApiKey = inputNeynarApiKey;
|
||||||
} else {
|
} else {
|
||||||
console.log('Using existing Neynar client ID from .env');
|
console.log('Using existing Neynar API key from .env');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!neynarApiKey) {
|
||||||
|
useNeynar = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to get client ID from API
|
||||||
|
const appInfo = await queryNeynarApp(neynarApiKey);
|
||||||
|
if (appInfo) {
|
||||||
|
neynarClientId = appInfo.app_uuid;
|
||||||
|
console.log('✅ Fetched Neynar app client ID');
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -197,7 +228,7 @@ async function main() {
|
|||||||
|
|
||||||
// Validate seed phrase and get account address
|
// Validate seed phrase and get account address
|
||||||
const accountAddress = await validateSeedPhrase(seedPhrase);
|
const accountAddress = await validateSeedPhrase(seedPhrase);
|
||||||
console.log('✅ Seed phrase validated successfully');
|
console.log('✅ Generated account address from seed phrase');
|
||||||
|
|
||||||
// Generate and sign manifest
|
// Generate and sign manifest
|
||||||
console.log('\n🔨 Generating frame manifest...');
|
console.log('\n🔨 Generating frame manifest...');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user