use neynar notifs

This commit is contained in:
lucas-neynar 2025-03-14 17:02:56 -07:00
parent 710c8255bf
commit c9deb0512c
No known key found for this signature in database
8 changed files with 187 additions and 212 deletions

View File

@ -1,4 +1,4 @@
# 🖼️ frames-v2-demo # Frames v2 Quickstart by Neynar 🪐
A Farcaster Frames v2 quickstart npx script. A Farcaster Frames v2 quickstart npx script.
@ -8,5 +8,5 @@ This is a [NextJS](https://nextjs.org/) + TypeScript + React app.
To create a new frames project, run: To create a new frames project, run:
```{bash} ```{bash}
npx frames-v2-demo npx create-neynar-farcaster-frame
``` ```

View File

@ -11,7 +11,7 @@ import { mnemonicToAccount } from 'viem/accounts';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const REPO_URL = 'https://github.com/lucas-neynar/frames-v2-quickstart.git'; const REPO_URL = 'https://github.com/neynarxyz/create-neynar-farcaster-frame.git';
const SCRIPT_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version; const SCRIPT_VERSION = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
function printWelcomeMessage() { function printWelcomeMessage() {
@ -24,7 +24,7 @@ function printWelcomeMessage() {
console.log(` console.log(`
${purple}${reset} ${purple}${reset}
${purple} ${reset} ${purple} ${reset}
${purple}${reset} ${bright}Welcome to Frames v2 Quickstart${reset} ${purple}${reset} ${purple}${reset} ${bright}Welcome to Frames v2 Quickstart by Neynar${reset} ${purple}${reset}
${purple}${reset} ${dim}The fastest way to build Farcaster Frames${reset} ${purple}${reset} ${purple}${reset} ${dim}The fastest way to build Farcaster Frames${reset} ${purple}${reset}
${purple} ${reset} ${purple} ${reset}
${purple}${reset} ${purple}${reset}
@ -112,31 +112,48 @@ async function init() {
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: null
},
{
type: 'confirm',
name: 'useNeynar',
message: 'Would you like to use Neynar in your frame?',
default: false
} }
]); ]);
// If using Neynar, ask for API key // Handle Neynar API key
if (answers.useNeynar) { const neynarFlow = await inquirer.prompt([
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 (neynarFlow.useNeynar) {
const neynarKeyAnswer = await inquirer.prompt([
{ {
type: 'password', type: 'password',
name: 'neynarApiKey', name: 'neynarApiKey',
message: 'Enter your Neynar API key:', message: 'Enter your Neynar API key (or press enter to skip):',
validate: (input) => { default: null
if (input.trim() === '') {
return 'Neynar API key cannot be empty';
}
return true;
}
} }
]); ]);
answers.neynarApiKey = neynarAnswers.neynarApiKey;
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;
}
} else {
answers.useNeynar = false;
answers.neynarApiKey = null;
} }
// Ask for seed phrase last // Ask for seed phrase last
@ -329,7 +346,7 @@ async function init() {
// Update README // Update README
console.log('\nUpdating README...'); console.log('\nUpdating README...');
const readmePath = path.join(projectPath, 'README.md'); const readmePath = path.join(projectPath, 'README.md');
const prependText = `<!-- generated by frames-v2-quickstart version ${SCRIPT_VERSION} -->\n\n`; const prependText = `<!-- generated by create-neynar-farcaster-frame version ${SCRIPT_VERSION} -->\n\n`;
if (fs.existsSync(readmePath)) { if (fs.existsSync(readmePath)) {
const originalReadmeContent = fs.readFileSync(readmePath, { encoding: 'utf8' }); const originalReadmeContent = fs.readFileSync(readmePath, { encoding: 'utf8' });
const updatedReadmeContent = prependText + originalReadmeContent; const updatedReadmeContent = prependText + originalReadmeContent;
@ -353,7 +370,7 @@ async function init() {
console.log('\nInitializing git repository...'); console.log('\nInitializing git repository...');
execSync('git init', { cwd: projectPath }); execSync('git init', { cwd: projectPath });
execSync('git add .', { cwd: projectPath }); execSync('git add .', { cwd: projectPath });
execSync('git commit -m "initial commit from frames-v2-quickstart"', { cwd: projectPath }); execSync('git commit -m "initial commit from create-neynar-farcaster-frame"', { cwd: projectPath });
// Calculate border length based on message length // Calculate border length based on message length
const message = `✨🪐 Successfully created frame ${projectName} with git and dependencies installed! 🪐✨`; const message = `✨🪐 Successfully created frame ${projectName} with git and dependencies installed! 🪐✨`;

234
package-lock.json generated
View File

@ -1,25 +1,25 @@
{ {
"name": "frames-v2-demo", "name": "create-neynar-farcaster-frame",
"version": "0.1.3", "version": "1.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frames-v2-demo", "name": "create-neynar-farcaster-frame",
"version": "0.1.3", "version": "1.0.0",
"dependencies": { "dependencies": {
"dotenv": "^16.4.7", "dotenv": "^16.4.7",
"inquirer": "^12.4.3", "inquirer": "^12.4.3",
"viem": "^2.23.6" "viem": "^2.23.6"
}, },
"bin": { "bin": {
"frames-v2-demo": "bin/index.js" "create-neynar-farcaster-frame": "bin/index.js"
} }
}, },
"node_modules/@adraffy/ens-normalize": { "node_modules/@adraffy/ens-normalize": {
"version": "1.10.1", "version": "1.11.0",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz",
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@inquirer/checkbox": { "node_modules/@inquirer/checkbox": {
@ -389,17 +389,6 @@
"url": "https://paulmillr.com/funding/" "url": "https://paulmillr.com/funding/"
} }
}, },
"node_modules/@types/node": {
"version": "20.17.7",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.17.7.tgz",
"integrity": "sha512-sZXXnpBFMKbao30dUAvzKbdwA2JM1fwUtVEq/kxKuPI5mMwZiRElCpTXb0Biq/LMEVpXDZL5G5V0RPnxKeyaYg==",
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/abitype": { "node_modules/abitype": {
"version": "1.0.8", "version": "1.0.8",
"resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz",
@ -436,18 +425,6 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/ansi-escapes/node_modules/type-fest": {
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/ansi-regex": { "node_modules/ansi-regex": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@ -472,21 +449,6 @@
"url": "https://github.com/chalk/ansi-styles?sponsor=1" "url": "https://github.com/chalk/ansi-styles?sponsor=1"
} }
}, },
"node_modules/bufferutil": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/bufferutil/-/bufferutil-4.0.8.tgz",
"integrity": "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
},
"engines": {
"node": ">=6.14.2"
}
},
"node_modules/chardet": { "node_modules/chardet": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz",
@ -532,6 +494,12 @@
"url": "https://dotenvx.com" "url": "https://dotenvx.com"
} }
}, },
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@ -623,19 +591,6 @@
"node": "^18.17.0 || >=20.5.0" "node": "^18.17.0 || >=20.5.0"
} }
}, },
"node_modules/node-gyp-build": {
"version": "4.8.4",
"resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
"integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
"license": "MIT",
"optional": true,
"peer": true,
"bin": {
"node-gyp-build": "bin.js",
"node-gyp-build-optional": "optional.js",
"node-gyp-build-test": "build-test.js"
}
},
"node_modules/os-tmpdir": { "node_modules/os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -645,6 +600,35 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ox": {
"version": "0.6.9",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.6.9.tgz",
"integrity": "sha512-wi5ShvzE4eOcTwQVsIPdFr+8ycyX+5le/96iAJutaZAvCes1J0+RvpEPg5QDPDiaR0XQQAvZVl7AwqQcINuUug==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "^1.10.1",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0",
"@scure/bip32": "^1.5.0",
"@scure/bip39": "^1.4.0",
"abitype": "^1.0.6",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/run-async": { "node_modules/run-async": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz",
@ -695,12 +679,6 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/string-width/node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
"license": "MIT"
},
"node_modules/strip-ansi": { "node_modules/strip-ansi": {
"version": "6.0.1", "version": "6.0.1",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
@ -726,53 +704,27 @@
} }
}, },
"node_modules/tslib": { "node_modules/tslib": {
"version": "2.7.0", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/typescript": { "node_modules/type-fest": {
"version": "5.7.2", "version": "0.21.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"license": "Apache-2.0", "license": "(MIT OR CC0-1.0)",
"optional": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": { "engines": {
"node": ">=14.17" "node": ">=10"
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"license": "MIT",
"optional": true,
"peer": true
},
"node_modules/utf-8-validate": {
"version": "5.0.10",
"resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz",
"integrity": "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ==",
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"peer": true,
"dependencies": {
"node-gyp-build": "^4.3.0"
}, },
"engines": { "funding": {
"node": ">=6.14.2" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/viem": { "node_modules/viem": {
"version": "2.23.6", "version": "2.23.10",
"resolved": "https://registry.npmjs.org/viem/-/viem-2.23.6.tgz", "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.10.tgz",
"integrity": "sha512-+yUeK8rktbGFQaLIvY4Tki22HUjian9Z4eKGAUT72RF9bcfkYgK8CJZz9P83tgoeLpiTyX3xcBM4xJZrJyKmsA==", "integrity": "sha512-va6Wde+v96PdfzdPEspCML1MjAqe+88O8BD+R9Kun/4s5KMUNcqfHbXdZP0ZZ2Zms80styvH2pDRAqCho6TqkA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@ -787,8 +739,8 @@
"@scure/bip39": "1.5.4", "@scure/bip39": "1.5.4",
"abitype": "1.0.8", "abitype": "1.0.8",
"isows": "1.0.6", "isows": "1.0.6",
"ox": "0.6.7", "ox": "0.6.9",
"ws": "8.18.0" "ws": "8.18.1"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": ">=5.0.4" "typescript": ">=5.0.4"
@ -799,56 +751,6 @@
} }
} }
}, },
"node_modules/viem/node_modules/ox": {
"version": "0.6.7",
"resolved": "https://registry.npmjs.org/ox/-/ox-0.6.7.tgz",
"integrity": "sha512-17Gk/eFsFRAZ80p5eKqv89a57uXjd3NgIf1CaXojATPBuujVc/fQSVhBeAU9JCRB+k7J50WQAyWTxK19T9GgbA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/wevm"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "^1.10.1",
"@noble/curves": "^1.6.0",
"@noble/hashes": "^1.5.0",
"@scure/bip32": "^1.5.0",
"@scure/bip39": "^1.4.0",
"abitype": "^1.0.6",
"eventemitter3": "5.0.1"
},
"peerDependencies": {
"typescript": ">=5.4.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/viem/node_modules/ws": {
"version": "8.18.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/wrap-ansi": { "node_modules/wrap-ansi": {
"version": "6.2.0", "version": "6.2.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
@ -864,11 +766,10 @@
} }
}, },
"node_modules/ws": { "node_modules/ws": {
"version": "8.17.1", "version": "8.18.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=10.0.0" "node": ">=10.0.0"
}, },
@ -896,17 +797,6 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
},
"node_modules/zod": {
"version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT",
"optional": true,
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
} }
} }
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "frames-v2-demo", "name": "create-neynar-farcaster-frame",
"version": "0.2.0", "version": "1.0.0",
"type": "module", "type": "module",
"files": [ "files": [
"bin/index.js" "bin/index.js"
@ -21,7 +21,7 @@
"lint": "next lint" "lint": "next lint"
}, },
"bin": { "bin": {
"frames-v2-demo": "./bin/index.js" "create-neynar-farcaster-frame": "./bin/index.js"
}, },
"dependencies": { "dependencies": {
"dotenv": "^16.4.7", "dotenv": "^16.4.7",

View File

@ -3,6 +3,7 @@ import { NextRequest } from "next/server";
import { z } from "zod"; import { z } from "zod";
import { setUserNotificationDetails } from "~/lib/kv"; import { setUserNotificationDetails } from "~/lib/kv";
import { sendFrameNotification } from "~/lib/notifs"; import { sendFrameNotification } from "~/lib/notifs";
import { sendNeynarFrameNotification } from "~/lib/neynar";
const requestSchema = z.object({ const requestSchema = z.object({
fid: z.number(), fid: z.number(),
@ -10,6 +11,10 @@ const requestSchema = z.object({
}); });
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to store notification details
// as they will be managed by Neynar's system
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const requestJson = await request.json(); const requestJson = await request.json();
const requestBody = requestSchema.safeParse(requestJson); const requestBody = requestSchema.safeParse(requestJson);
@ -20,13 +25,18 @@ export async function POST(request: NextRequest) {
); );
} }
await setUserNotificationDetails( // Only store notification details if not using Neynar
requestBody.data.fid, if (!neynarEnabled) {
requestBody.data.notificationDetails await setUserNotificationDetails(
); Number(requestBody.data.fid),
requestBody.data.notificationDetails
);
}
const sendResult = await sendFrameNotification({ // Use appropriate notification function based on Neynar status
fid: requestBody.data.fid, const sendNotification = neynarEnabled ? sendNeynarFrameNotification : sendFrameNotification;
const sendResult = await sendNotification({
fid: Number(requestBody.data.fid),
title: "Test notification", title: "Test notification",
body: "Sent at " + new Date().toISOString(), body: "Sent at " + new Date().toISOString(),
}); });

View File

@ -11,6 +11,13 @@ import {
import { sendFrameNotification } from "~/lib/notifs"; import { sendFrameNotification } from "~/lib/notifs";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to handle webhooks here
// as they will be handled by Neynar's webhook endpoint
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
if (neynarEnabled) {
return Response.json({ success: true });
}
const requestJson = await request.json(); const requestJson = await request.json();
let data; let data;
@ -45,6 +52,8 @@ export async function POST(request: NextRequest) {
const fid = data.fid; const fid = data.fid;
const event = data.event; const event = data.event;
// Only handle notifications if Neynar is not enabled
// When Neynar is enabled, notifications are handled through their webhook
switch (event.event) { switch (event.event) {
case "frame_added": case "frame_added":
if (event.notificationDetails) { if (event.notificationDetails) {
@ -57,12 +66,12 @@ export async function POST(request: NextRequest) {
} else { } else {
await deleteUserNotificationDetails(fid); await deleteUserNotificationDetails(fid);
} }
break; break;
case "frame_removed": case "frame_removed":
await deleteUserNotificationDetails(fid); await deleteUserNotificationDetails(fid);
break; break;
case "notifications_enabled": case "notifications_enabled":
await setUserNotificationDetails(fid, event.notificationDetails); await setUserNotificationDetails(fid, event.notificationDetails);
await sendFrameNotification({ await sendFrameNotification({
@ -70,11 +79,10 @@ export async function POST(request: NextRequest) {
title: "Ding ding ding", title: "Ding ding ding",
body: "Notifications are now enabled", body: "Notifications are now enabled",
}); });
break; break;
case "notifications_disabled": case "notifications_disabled":
await deleteUserNotificationDetails(fid); await deleteUserNotificationDetails(fid);
break; break;
} }

View File

@ -2,6 +2,9 @@ import { NeynarAPIClient } from '@neynar/nodejs-sdk';
let neynarClient: NeynarAPIClient | null = null; let neynarClient: NeynarAPIClient | null = null;
// Example usage:
// const client = getNeynarClient();
// const user = await client.lookupUserByFid(fid);
export function getNeynarClient() { export function getNeynarClient() {
if (!neynarClient) { if (!neynarClient) {
const apiKey = process.env.NEYNAR_API_KEY; const apiKey = process.env.NEYNAR_API_KEY;
@ -13,6 +16,46 @@ export function getNeynarClient() {
return neynarClient; return neynarClient;
} }
// Example usage: type SendFrameNotificationResult =
// const client = getNeynarClient(); | {
// const user = await client.lookupUserByFid(fid); state: "error";
error: unknown;
}
| { state: "no_token" }
| { state: "rate_limit" }
| { state: "success" };
export async function sendNeynarFrameNotification({
fid,
title,
body,
}: {
fid: number;
title: string;
body: string;
}): Promise<SendFrameNotificationResult> {
try {
const client = getNeynarClient();
const targetFids = [fid];
const notification = {
title,
body,
target_url: process.env.NEXT_PUBLIC_URL,
};
const result = await client.publishFrameNotifications({
targetFids,
notification
});
if (result.success) {
return { state: "success" };
} else if (result.status === 429) {
return { state: "rate_limit" };
} else {
return { state: "error", error: result.error || "Unknown error" };
}
} catch (error) {
return { state: "error", error };
}
}

View File

@ -62,6 +62,13 @@ export async function generateFarcasterMetadata() {
}; };
} }
// Determine webhook URL based on whether Neynar is enabled
const neynarApiKey = process.env.NEYNAR_API_KEY;
const neynarClientId = process.env.NEYNAR_CLIENT_ID;
const webhookUrl = neynarApiKey && neynarClientId
? `https://api.neynar.com/f/app/${neynarClientId}/event`
: `${appUrl}/api/webhook`;
return { return {
accountAssociation, accountAssociation,
frame: { frame: {
@ -73,7 +80,7 @@ export async function generateFarcasterMetadata() {
buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT || "Launch Frame", buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT || "Launch Frame",
splashImageUrl: process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL || `${appUrl}/splash.png`, splashImageUrl: process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL || `${appUrl}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",
webhookUrl: `${appUrl}/api/webhook`, webhookUrl,
}, },
}; };
} }