use neynar notifs

This commit is contained in:
lucas-neynar
2025-03-14 17:02:56 -07:00
parent 710c8255bf
commit c9deb0512c
8 changed files with 187 additions and 212 deletions

View File

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

View File

@@ -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;
}

View File

@@ -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);
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<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 {
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,
},
};
}