Add addFrame() action, sending notifications and getting webhook events

This commit is contained in:
Christian Mladenov 2024-12-02 23:17:14 -08:00 committed by lucas-neynar
parent 53ffc28623
commit 6691998a1d
No known key found for this signature in database
4 changed files with 232 additions and 1 deletions

View File

@ -0,0 +1,66 @@
import {
SendNotificationRequest,
sendNotificationResponseSchema,
} from "@farcaster/frame-sdk";
import { NextRequest } from "next/server";
import { z } from "zod";
const requestSchema = z.object({
token: z.string(),
url: z.string(),
targetUrl: z.string(),
});
export async function POST(request: NextRequest) {
const requestJson = await request.json();
const requestBody = requestSchema.safeParse(requestJson);
if (requestBody.success === false) {
return Response.json(
{ success: false, errors: requestBody.error.errors },
{ status: 400 }
);
}
const response = await fetch(requestBody.data.url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
notificationId: crypto.randomUUID(),
title: "Hello from Frames v2!",
body: "This is a test notification",
targetUrl: requestBody.data.targetUrl,
tokens: [requestBody.data.token],
} satisfies SendNotificationRequest),
});
const responseJson = await response.json();
if (response.status === 200) {
// Ensure correct response
const responseBody = sendNotificationResponseSchema.safeParse(responseJson);
if (responseBody.success === false) {
return Response.json(
{ success: false, errors: responseBody.error.errors },
{ status: 500 }
);
}
// Fail when rate limited
if (responseBody.data.result.rateLimitedTokens.length) {
return Response.json(
{ success: false, error: "Rate limited" },
{ status: 429 }
);
}
return Response.json({ success: true });
} else {
return Response.json(
{ success: false, error: responseJson },
{ status: 500 }
);
}
}

View File

@ -0,0 +1,72 @@
import {
eventHeaderSchema,
eventPayloadSchema,
eventSchema,
} from "@farcaster/frame-sdk";
import { NextRequest } from "next/server";
export async function POST(request: NextRequest) {
const requestJson = await request.json();
const requestBody = eventSchema.safeParse(requestJson);
if (requestBody.success === false) {
return Response.json(
{ success: false, errors: requestBody.error.errors },
{ status: 400 }
);
}
// TODO: verify signature
const headerData = JSON.parse(
Buffer.from(requestBody.data.header, "base64url").toString("utf-8")
);
const header = eventHeaderSchema.safeParse(headerData);
if (header.success === false) {
return Response.json(
{ success: false, errors: header.error.errors },
{ status: 400 }
);
}
const fid = header.data.fid;
const payloadData = JSON.parse(
Buffer.from(requestBody.data.payload, "base64url").toString("utf-8")
);
const payload = eventPayloadSchema.safeParse(payloadData);
if (payload.success === false) {
return Response.json(
{ success: false, errors: payload.error.errors },
{ status: 400 }
);
}
switch (payload.data.event) {
case "frame-added":
console.log(
payload.data.notificationDetails
? `Got frame-added event for fid ${fid} with notification token ${payload.data.notificationDetails.token} and url ${payload.data.notificationDetails.url}`
: `Got frame-added event for fid ${fid} with no notification details`
);
break;
case "frame-removed":
console.log(`Got frame-removed event for fid ${fid}`);
break;
case "notifications-enabled":
console.log(
`Got notifications-enabled event for fid ${fid} with token ${
payload.data.notificationDetails.token
} and url ${payload.data.notificationDetails.url} ${JSON.stringify(
payload.data
)}`
);
break;
case "notifications-disabled":
console.log(`Got notifications-disabled event for fid ${fid}`);
break;
}
return Response.json({ success: true });
}

View File

@ -1,5 +1,8 @@
import { useEffect, useCallback, useState } from "react"; import { useEffect, useCallback, useState } from "react";
import sdk, { type FrameContext } from "@farcaster/frame-sdk"; import sdk, {
FrameNotificationDetails,
type FrameContext,
} from "@farcaster/frame-sdk";
import { import {
useAccount, useAccount,
useSendTransaction, useSendTransaction,
@ -21,6 +24,10 @@ export default function Demo(
const [context, setContext] = useState<FrameContext>(); const [context, setContext] = useState<FrameContext>();
const [isContextOpen, setIsContextOpen] = useState(false); const [isContextOpen, setIsContextOpen] = useState(false);
const [txHash, setTxHash] = useState<string | null>(null); const [txHash, setTxHash] = useState<string | null>(null);
const [addFrameResult, setAddFrameResult] = useState("");
const [notificationDetails, setNotificationDetails] =
useState<FrameNotificationDetails | null>(null);
const [sendNotificationResult, setSendNotificationResult] = useState("");
const { address, isConnected } = useAccount(); const { address, isConnected } = useAccount();
const { const {
@ -75,6 +82,60 @@ export default function Demo(
sdk.actions.close(); sdk.actions.close();
}, []); }, []);
const addFrame = useCallback(async () => {
try {
// setAddFrameResult("");
setNotificationDetails(null);
const result = await sdk.actions.addFrame();
if (result.added) {
if (result.notificationDetails) {
setNotificationDetails(result.notificationDetails);
}
setAddFrameResult(
result.notificationDetails
? `Added, got notificaton token ${result.notificationDetails.token} and url ${result.notificationDetails.url}`
: "Added, got no notification details"
);
} else {
setAddFrameResult(`Not added: ${result.reason}`);
}
} catch (error) {
setAddFrameResult(`Error: ${error}`);
}
}, []);
const sendNotification = useCallback(async () => {
setSendNotificationResult("");
if (!notificationDetails) {
return;
}
try {
const response = await fetch("/api/send-notification", {
method: "POST",
mode: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
token: notificationDetails.token,
url: notificationDetails.url,
targetUrl: window.location.href,
}),
});
if (response.status === 200) {
setSendNotificationResult("Success");
return;
}
const data = await response.text();
setSendNotificationResult(`Error: ${data}`);
} catch (error) {
setSendNotificationResult(`Error: ${error}`);
}
}, [notificationDetails]);
const sendTx = useCallback(() => { const sendTx = useCallback(() => {
sendTransaction( sendTransaction(
{ {
@ -181,8 +242,35 @@ export default function Demo(
</div> </div>
<Button onClick={close}>Close Frame</Button> <Button onClick={close}>Close Frame</Button>
</div> </div>
<div className="mb-4">
<div className="p-2 bg-gray-100 dark:bg-gray-800 rounded-lg my-2">
<pre className="font-mono text-xs whitespace-pre-wrap break-words max-w-[260px] overflow-x-">
sdk.actions.addFrame
</pre>
</div>
{addFrameResult && (
<div className="mb-2">Add frame result: {addFrameResult}</div>
)}
<Button onClick={addFrame}>Add frame to client</Button>
</div>
</div> </div>
{notificationDetails && (
<div>
<h2 className="font-2xl font-bold">Notify</h2>
{sendNotificationResult && (
<div className="mb-2">
Send notification result: {sendNotificationResult}
</div>
)}
<div className="mb-4">
<Button onClick={sendNotification}>Send notification</Button>
</div>
</div>
)}
<div> <div>
<h2 className="font-2xl font-bold">Wallet</h2> <h2 className="font-2xl font-bold">Wallet</h2>

View File

@ -5242,6 +5242,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@^3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zustand@5.0.0: zustand@5.0.0:
version "5.0.0" version "5.0.0"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0.tgz#71f8aaecf185592a3ba2743d7516607361899da9" resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0.tgz#71f8aaecf185592a3ba2743d7516607361899da9"