From c9deb0512ced42c8e1fd97763308a3683f05a6db Mon Sep 17 00:00:00 2001 From: lucas-neynar Date: Fri, 14 Mar 2025 17:02:56 -0700 Subject: [PATCH] use neynar notifs --- README.md | 4 +- bin/index.js | 59 ++++--- package-lock.json | 234 +++++++------------------ package.json | 6 +- src/app/api/send-notification/route.ts | 22 ++- src/app/api/webhook/route.ts | 16 +- src/lib/neynar.ts | 49 +++++- src/lib/utils.ts | 9 +- 8 files changed, 187 insertions(+), 212 deletions(-) diff --git a/README.md b/README.md index 15fb215..7dbc19a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 🖼️ frames-v2-demo +# Frames v2 Quickstart by Neynar 🪐 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: ```{bash} -npx frames-v2-demo +npx create-neynar-farcaster-frame ``` diff --git a/bin/index.js b/bin/index.js index 5c3bbde..b0f9c1d 100755 --- a/bin/index.js +++ b/bin/index.js @@ -11,7 +11,7 @@ import { mnemonicToAccount } from 'viem/accounts'; const __filename = fileURLToPath(import.meta.url); 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; function printWelcomeMessage() { @@ -24,7 +24,7 @@ function printWelcomeMessage() { console.log(` ${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} ${purple}╚═══════════════════════════════════════════════════╝${reset} @@ -112,31 +112,48 @@ async function init() { 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:', 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 - if (answers.useNeynar) { - const neynarAnswers = await inquirer.prompt([ + // 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:', - validate: (input) => { - if (input.trim() === '') { - return 'Neynar API key cannot be empty'; - } - return true; - } + message: 'Enter your Neynar API key (or press enter to skip):', + default: null } ]); - 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 @@ -329,7 +346,7 @@ async function init() { // Update README console.log('\nUpdating README...'); const readmePath = path.join(projectPath, 'README.md'); - const prependText = `\n\n`; + const prependText = `\n\n`; if (fs.existsSync(readmePath)) { const originalReadmeContent = fs.readFileSync(readmePath, { encoding: 'utf8' }); const updatedReadmeContent = prependText + originalReadmeContent; @@ -353,7 +370,7 @@ async function init() { console.log('\nInitializing git repository...'); execSync('git init', { 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 const message = `✨🪐 Successfully created frame ${projectName} with git and dependencies installed! 🪐✨`; diff --git a/package-lock.json b/package-lock.json index cf164c0..4c19e46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,25 +1,25 @@ { - "name": "frames-v2-demo", - "version": "0.1.3", + "name": "create-neynar-farcaster-frame", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "frames-v2-demo", - "version": "0.1.3", + "name": "create-neynar-farcaster-frame", + "version": "1.0.0", "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", "viem": "^2.23.6" }, "bin": { - "frames-v2-demo": "bin/index.js" + "create-neynar-farcaster-frame": "bin/index.js" } }, "node_modules/@adraffy/ens-normalize": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", - "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==", + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", + "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", "license": "MIT" }, "node_modules/@inquirer/checkbox": { @@ -389,17 +389,6 @@ "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": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", @@ -436,18 +425,6 @@ "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": { "version": "5.0.1", "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" } }, - "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": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", @@ -532,6 +494,12 @@ "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": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", @@ -623,19 +591,6 @@ "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": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", @@ -645,6 +600,35 @@ "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": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/run-async/-/run-async-3.0.0.tgz", @@ -695,12 +679,6 @@ "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": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -726,53 +704,27 @@ } }, "node_modules/tslib": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz", - "integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==", + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "license": "Apache-2.0", - "optional": true, - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, + "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": ">=14.17" - } - }, - "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" + "node": ">=10" }, - "engines": { - "node": ">=6.14.2" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/viem": { - "version": "2.23.6", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.6.tgz", - "integrity": "sha512-+yUeK8rktbGFQaLIvY4Tki22HUjian9Z4eKGAUT72RF9bcfkYgK8CJZz9P83tgoeLpiTyX3xcBM4xJZrJyKmsA==", + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.23.10.tgz", + "integrity": "sha512-va6Wde+v96PdfzdPEspCML1MjAqe+88O8BD+R9Kun/4s5KMUNcqfHbXdZP0ZZ2Zms80styvH2pDRAqCho6TqkA==", "funding": [ { "type": "github", @@ -787,8 +739,8 @@ "@scure/bip39": "1.5.4", "abitype": "1.0.8", "isows": "1.0.6", - "ox": "0.6.7", - "ws": "8.18.0" + "ox": "0.6.9", + "ws": "8.18.1" }, "peerDependencies": { "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": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -864,11 +766,10 @@ } }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", + "integrity": "sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w==", "license": "MIT", - "peer": true, "engines": { "node": ">=10.0.0" }, @@ -896,17 +797,6 @@ "funding": { "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" - } } } } diff --git a/package.json b/package.json index 41a5726..a92ea24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { - "name": "frames-v2-demo", - "version": "0.2.0", + "name": "create-neynar-farcaster-frame", + "version": "1.0.0", "type": "module", "files": [ "bin/index.js" @@ -21,7 +21,7 @@ "lint": "next lint" }, "bin": { - "frames-v2-demo": "./bin/index.js" + "create-neynar-farcaster-frame": "./bin/index.js" }, "dependencies": { "dotenv": "^16.4.7", diff --git a/src/app/api/send-notification/route.ts b/src/app/api/send-notification/route.ts index 3e01348..3dda0f3 100644 --- a/src/app/api/send-notification/route.ts +++ b/src/app/api/send-notification/route.ts @@ -3,6 +3,7 @@ import { NextRequest } from "next/server"; import { z } from "zod"; import { setUserNotificationDetails } from "~/lib/kv"; import { sendFrameNotification } from "~/lib/notifs"; +import { sendNeynarFrameNotification } from "~/lib/neynar"; const requestSchema = z.object({ fid: z.number(), @@ -10,6 +11,10 @@ const requestSchema = z.object({ }); 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 requestBody = requestSchema.safeParse(requestJson); @@ -20,13 +25,18 @@ export async function POST(request: NextRequest) { ); } - await setUserNotificationDetails( - requestBody.data.fid, - requestBody.data.notificationDetails - ); + // Only store notification details if not using Neynar + if (!neynarEnabled) { + await setUserNotificationDetails( + Number(requestBody.data.fid), + requestBody.data.notificationDetails + ); + } - const sendResult = await sendFrameNotification({ - fid: requestBody.data.fid, + // Use appropriate notification function based on Neynar status + const sendNotification = neynarEnabled ? sendNeynarFrameNotification : sendFrameNotification; + const sendResult = await sendNotification({ + fid: Number(requestBody.data.fid), title: "Test notification", body: "Sent at " + new Date().toISOString(), }); diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index 2bdc294..af6973c 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -11,6 +11,13 @@ import { import { sendFrameNotification } from "~/lib/notifs"; 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(); let data; @@ -45,6 +52,8 @@ export async function POST(request: NextRequest) { const fid = data.fid; 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) { case "frame_added": if (event.notificationDetails) { @@ -57,12 +66,12 @@ export async function POST(request: NextRequest) { } else { await deleteUserNotificationDetails(fid); } - break; + case "frame_removed": await deleteUserNotificationDetails(fid); - break; + case "notifications_enabled": await setUserNotificationDetails(fid, event.notificationDetails); await sendFrameNotification({ @@ -70,11 +79,10 @@ export async function POST(request: NextRequest) { title: "Ding ding ding", body: "Notifications are now enabled", }); - break; + case "notifications_disabled": await deleteUserNotificationDetails(fid); - break; } diff --git a/src/lib/neynar.ts b/src/lib/neynar.ts index cfc87c5..5cba8f2 100644 --- a/src/lib/neynar.ts +++ b/src/lib/neynar.ts @@ -2,6 +2,9 @@ import { NeynarAPIClient } from '@neynar/nodejs-sdk'; let neynarClient: NeynarAPIClient | null = null; +// Example usage: +// const client = getNeynarClient(); +// const user = await client.lookupUserByFid(fid); export function getNeynarClient() { if (!neynarClient) { const apiKey = process.env.NEYNAR_API_KEY; @@ -13,6 +16,46 @@ export function getNeynarClient() { return neynarClient; } -// Example usage: -// const client = getNeynarClient(); -// const user = await client.lookupUserByFid(fid); \ No newline at end of file +type SendFrameNotificationResult = + | { + 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 { + 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 }; + } +} \ No newline at end of file diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 848bc55..a81b9fe 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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 { accountAssociation, frame: { @@ -73,7 +80,7 @@ export async function generateFarcasterMetadata() { buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT || "Launch Frame", splashImageUrl: process.env.NEXT_PUBLIC_FRAME_SPLASH_IMAGE_URL || `${appUrl}/splash.png`, splashBackgroundColor: "#f7f7f7", - webhookUrl: `${appUrl}/api/webhook`, + webhookUrl, }, }; }