diff --git a/.changeset/clean-dragons-return.md b/.changeset/clean-dragons-return.md new file mode 100644 index 000000000..9e08bda4c --- /dev/null +++ b/.changeset/clean-dragons-return.md @@ -0,0 +1,5 @@ +--- +"@frames.js/debugger": patch +--- + +feat: view profile dialog diff --git a/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx new file mode 100644 index 000000000..437cc40b8 --- /dev/null +++ b/packages/debugger/app/components/frame-app-debugger-view-profile-dialog.tsx @@ -0,0 +1,118 @@ +/* eslint-disable @next/next/no-img-element */ +import React from "react"; +import { + Dialog, + DialogContent, + DialogTitle, + DialogDescription, +} from "@/components/ui/dialog"; +import { useQuery } from "@tanstack/react-query"; +import { z } from "zod"; +import { Loader2Icon, TriangleAlertIcon } from "lucide-react"; +import Image from "next/image"; + +type UserDetails = { + username: string; + pfp_url: string; + profile: { + bio: { + text: string; + }; + }; + follower_count: number; + following_count: number; +}; + +type FrameAppDebuggerViewProfileDialogProps = { + fid: number; + onDismiss: () => void; +}; + +const UserDetailsSchema = z.object({ + username: z.string(), + pfp_url: z.string().url(), + profile: z.object({ + bio: z.object({ + text: z.string(), + }), + }), + follower_count: z.number().int(), + following_count: z.number().int(), +}); + +async function fetchUser(fid: number): Promise { + const response = await fetch(`/farcaster/user/${fid}`); + + if (!response.ok) { + throw new Error("Network response was not ok"); + } + + const data = await response.json(); + + return UserDetailsSchema.parse(data); +} + +export function FrameAppDebuggerViewProfileDialog({ + fid, + onDismiss, +}: FrameAppDebuggerViewProfileDialogProps) { + const query = useQuery({ + queryKey: ["user", fid], + queryFn: () => fetchUser(fid), + }); + + return ( + + + Profile Details + {query.isLoading && ( + + + + )} + {query.isError && ( + + + Unexpected error occurred + + )} + {query.isSuccess && ( + +
+
+ {query.data.username} +
+
+ {query.data.username} +
+
+ {formatCount(query.data.follower_count)}{" "} + followers +
+
+ {formatCount(query.data.following_count)}{" "} + following +
+
+ {query.data.profile.bio.text} +
+ )} +
+
+ ); +} + +function formatCount(count: number): string { + if (count < 1000) { + return count.toString(); + } else if (count >= 1000 && count < 1000000) { + return (count / 1000).toFixed(1) + "K"; + } + + return (count / 1000000).toFixed(1) + "M"; +} diff --git a/packages/debugger/app/components/frame-app-debugger.tsx b/packages/debugger/app/components/frame-app-debugger.tsx index 6f710b042..8c2bc7e5c 100644 --- a/packages/debugger/app/components/frame-app-debugger.tsx +++ b/packages/debugger/app/components/frame-app-debugger.tsx @@ -30,6 +30,7 @@ import type { EIP6963ProviderInfo } from "@farcaster/frame-sdk"; import { z } from "zod"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { useCopyToClipboard } from "../hooks/useCopyToClipboad"; +import { FrameAppDebuggerViewProfileDialog } from "./frame-app-debugger-view-profile-dialog"; type TabValues = "events" | "console" | "notifications"; @@ -153,6 +154,7 @@ export function FrameAppDebugger({ }; } }, [toast]); + const [viewFidProfile, setViewFidProfile] = useState(null); const frameApp = useFrameAppInIframe({ debug: true, source: context.parseResult, @@ -289,6 +291,11 @@ export function FrameAppDebugger({ }); }, async onSignIn({ nonce, notBefore, expirationTime, frame }) { + console.info("sdk.actions.signIn() called", { + nonce, + notBefore, + expirationTime, + }); let abortTimeout: NodeJS.Timeout | undefined; try { @@ -373,6 +380,10 @@ export function FrameAppDebugger({ setFarcasterSignInAbortControllerURL(null); } }, + async onViewProfile(params) { + console.info("sdk.actions.viewProfile() called", params); + setViewFidProfile(params.fid); + }, }); return ( @@ -552,6 +563,14 @@ export function FrameAppDebugger({ ) : null} + {viewFidProfile !== null && ( + { + setViewFidProfile(null); + }} + /> + )} ); } diff --git a/packages/debugger/app/farcaster/user/[fid]/route.ts b/packages/debugger/app/farcaster/user/[fid]/route.ts new file mode 100644 index 000000000..f67f9e967 --- /dev/null +++ b/packages/debugger/app/farcaster/user/[fid]/route.ts @@ -0,0 +1,44 @@ +import type { NextRequest } from "next/server"; +import { z } from "zod"; + +const validator = z.object({ + fid: z.coerce.number().int().positive(), +}); + +export async function GET( + _: NextRequest, + { params }: { params: { fid: string } } +) { + try { + const { fid } = validator.parse(params); + + const url = new URL("https://api.neynar.com/v2/farcaster/user/bulk"); + + url.searchParams.set("fids", fid.toString()); + + const response = await fetch(url, { + headers: { + accept: "application/json", + "x-api-key": "NEYNAR_FRAMES_JS", + }, + }); + + if (!response.ok) { + if (response.status === 404) { + return Response.json({ error: "User not found" }, { status: 404 }); + } + + throw new Error(`Unexpected response: ${response.status}`); + } + + const data = await response.json(); + + return Response.json(data.users[0]); + } catch (e) { + if (e instanceof z.ZodError) { + return Response.json({ error: e.errors }, { status: 400 }); + } + + throw e; + } +}