mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
Add addFrame() action, sending notifications and getting webhook events
This commit is contained in:
parent
53ffc28623
commit
6691998a1d
66
src/app/api/send-notification/route.ts
Normal file
66
src/app/api/send-notification/route.ts
Normal 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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
src/app/api/webhook/route.ts
Normal file
72
src/app/api/webhook/route.ts
Normal 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 });
|
||||||
|
}
|
||||||
@ -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,7 +242,34 @@ 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>
|
</div>
|
||||||
|
{addFrameResult && (
|
||||||
|
<div className="mb-2">Add frame result: {addFrameResult}</div>
|
||||||
|
)}
|
||||||
|
<Button onClick={addFrame}>Add frame to client</Button>
|
||||||
|
</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>
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user