Skip to content

Commit 2d87f5b

Browse files
authored
Add quickAuth example to MiniKit example app (coinbase#2429)
1 parent 4895287 commit 2d87f5b

File tree

23 files changed

+354
-84
lines changed

23 files changed

+354
-84
lines changed

.changeset/blue-hands-sing.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"create-onchain": patch
3+
---
4+
5+
Update @farcaster/frame-sdk dependency

.changeset/green-socks-call.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@coinbase/onchainkit": patch
3+
---
4+
5+
Update @farcaster/frame-sdk dependency

examples/minikit-example/.env.example

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME=minikit-example
44
NEXT_PUBLIC_URL=
55
NEXT_PUBLIC_ICON_URL=$NEXT_PUBLIC_URL/logo.png
6-
NEXT_PUBLIC_ONCHAINKIT_API_KEY=""
6+
NEXT_PUBLIC_ONCHAINKIT_API_KEY=
77

88
# Frame metadata
99

@@ -23,6 +23,10 @@ NEXT_PUBLIC_APP_OG_TITLE=minikit-example
2323
NEXT_PUBLIC_APP_OG_DESCRIPTION=
2424
NEXT_PUBLIC_APP_OG_IMAGE=$NEXT_PUBLIC_URL/hero.png
2525

26+
# Neynar
27+
28+
NEYNAR_API_KEY=
29+
2630
# Redis config
2731

2832
REDIS_URL=
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { Errors, createClient } from "@farcaster/quick-auth";
2+
import { NextRequest, NextResponse } from "next/server";
3+
4+
const client = createClient();
5+
6+
function getUrlHost() {
7+
let urlValue: string;
8+
9+
if (process.env.VERCEL_ENV === "production") {
10+
urlValue = process.env.NEXT_PUBLIC_URL!;
11+
} else if (process.env.VERCEL_URL) {
12+
urlValue = `https://${process.env.VERCEL_URL}`;
13+
} else {
14+
urlValue = "http://localhost:3000";
15+
}
16+
17+
const url = new URL(urlValue);
18+
19+
return url.host;
20+
}
21+
22+
export async function GET(request: NextRequest) {
23+
// Because we're fetching this endpoint via `sdk.quickAuth.fetch`,
24+
// if we're in a mini app, the request will include the necessary `Authorization` header.
25+
const authorization = request.headers.get("Authorization");
26+
27+
// Here we ensure that we have a valid token.
28+
if (!authorization || !authorization.startsWith("Bearer ")) {
29+
return NextResponse.json({ message: "Missing token" }, { status: 401 });
30+
}
31+
32+
try {
33+
// Now we verify the token. `domain` must match the domain of the request.
34+
// In our case, we're using the `getUrlHost` function to get the domain of the request
35+
// based on the Vercel environment. This will vary depending on your hosting provider.
36+
const payload = await client.verifyJwt({
37+
token: authorization.split(" ")[1] as string,
38+
domain: getUrlHost(),
39+
});
40+
41+
// If the token was valid, `payload.sub` will be the user's Farcaster ID.
42+
const userFid = payload.sub;
43+
44+
// And now we can use that FID to do whatever we want.
45+
// In this example, we're going to get the user's info from Neynar and return it.
46+
const userInfoResult = await fetch(
47+
`https://api.neynar.com/v2/farcaster/user/bulk?fids=${userFid}`,
48+
{
49+
headers: {
50+
"x-api-key": process.env.NEYNAR_API_KEY || "",
51+
},
52+
},
53+
).then((res) => res.json());
54+
55+
const userInfo = userInfoResult?.users?.[0];
56+
57+
if (!userInfo) {
58+
return NextResponse.json({ message: "User not found" }, { status: 404 });
59+
}
60+
61+
return NextResponse.json(userInfo);
62+
} catch (e) {
63+
if (e instanceof Errors.InvalidTokenError) {
64+
return NextResponse.json({ message: "Invalid token" }, { status: 401 });
65+
}
66+
67+
if (e instanceof Error) {
68+
return NextResponse.json({ message: e.message }, { status: 500 });
69+
}
70+
71+
throw e;
72+
}
73+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { useIsInMiniApp } from "@coinbase/onchainkit/minikit";
2+
import sdk from "@farcaster/frame-sdk";
3+
import { useQuery } from "@tanstack/react-query";
4+
5+
function useUserInfo() {
6+
const { isInMiniApp } = useIsInMiniApp();
7+
8+
return useQuery({
9+
queryKey: ["useQuickAuth", isInMiniApp],
10+
queryFn: async () => {
11+
// If we're in a mini app context, all we have to do to make an authenticated
12+
// request is to use `sdk.quickAuth.fetch`. This will automatically include the
13+
// necessary `Authorization` header for the backend to verify.
14+
const result = await sdk.quickAuth.fetch("/api/me");
15+
16+
const userInfo = await result.json();
17+
return {
18+
displayName: userInfo.display_name,
19+
pfpUrl: userInfo.pfp_url,
20+
bio: userInfo.profile?.bio?.text,
21+
followerCount: userInfo.follower_count,
22+
followingCount: userInfo.following_count,
23+
};
24+
},
25+
enabled: isInMiniApp,
26+
});
27+
}
28+
29+
export function UserInfo() {
30+
const { data, isLoading, error } = useUserInfo();
31+
32+
if (isLoading) {
33+
return (
34+
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-4 animate-pulse">
35+
<div className="flex items-center space-x-4">
36+
<div className="w-16 h-16 bg-[var(--app-gray)] rounded-full"></div>
37+
<div className="space-y-2 flex-1">
38+
<div className="h-5 bg-[var(--app-gray)] rounded w-32"></div>
39+
<div className="h-4 bg-[var(--app-gray)] rounded w-24"></div>
40+
</div>
41+
</div>
42+
</div>
43+
);
44+
}
45+
46+
if (error || !data) {
47+
return (
48+
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-4">
49+
<p className="text-[var(--app-foreground-muted)] text-center">
50+
{error ? "Failed to load user info" : "No user info available"}
51+
</p>
52+
</div>
53+
);
54+
}
55+
56+
return (
57+
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-6 backdrop-blur-sm">
58+
<div className="flex items-start space-x-4">
59+
{/* Profile Picture */}
60+
{data.pfpUrl && (
61+
<img
62+
src={data.pfpUrl}
63+
alt={`${data.displayName}'s profile picture`}
64+
className="w-16 h-16 rounded-full object-cover border-2 border-[var(--app-card-border)]"
65+
/>
66+
)}
67+
68+
{/* User Info */}
69+
<div className="flex-1 min-w-0">
70+
{/* Display Name */}
71+
<h2 className="text-xl font-bold text-[var(--app-foreground)] mb-2 truncate">
72+
{data.displayName}
73+
</h2>
74+
75+
{/* Bio */}
76+
{data.bio && (
77+
<p className="text-[var(--app-foreground-muted)] text-sm mb-3 leading-relaxed">
78+
{data.bio}
79+
</p>
80+
)}
81+
82+
{/* Follower Stats */}
83+
<div className="flex space-x-6 text-sm">
84+
<div className="flex flex-col items-center">
85+
<span className="font-semibold text-[var(--app-foreground)]">
86+
{data.followerCount?.toLocaleString() || "0"}
87+
</span>
88+
<span className="text-[var(--app-foreground-muted)]">
89+
Followers
90+
</span>
91+
</div>
92+
<div className="flex flex-col items-center">
93+
<span className="font-semibold text-[var(--app-foreground)]">
94+
{data.followingCount?.toLocaleString() || "0"}
95+
</span>
96+
<span className="text-[var(--app-foreground-muted)]">
97+
Following
98+
</span>
99+
</div>
100+
</div>
101+
</div>
102+
</div>
103+
</div>
104+
);
105+
}

examples/minikit-example/app/page.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { AddFrame } from "./actions/AddFrame";
2020
import { ComposeCast } from "./actions/ComposeCast";
2121
import { ViewCast } from "./actions/ViewCast";
2222
import { CloseFrame } from "./actions/CloseFrame";
23+
import { UserInfo } from "./components/UserInfo";
2324

2425
export default function App() {
2526
const { setFrameReady, isFrameReady } = useMiniKit();
@@ -68,6 +69,7 @@ export default function App() {
6869

6970
<div className="flex flex-col gap-3 justify-center items-stretch max-w-md mx-auto">
7071
<IsInMiniApp />
72+
<UserInfo />
7173
<AddFrame />
7274
<ComposeCast />
7375
<ViewCast />

examples/minikit-example/lib/notification-client.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
FrameNotificationDetails,
2+
MiniAppNotificationDetails,
33
type SendNotificationRequest,
44
sendNotificationResponseSchema,
55
} from "@farcaster/frame-sdk";
@@ -25,7 +25,7 @@ export async function sendFrameNotification({
2525
fid: number;
2626
title: string;
2727
body: string;
28-
notificationDetails?: FrameNotificationDetails | null;
28+
notificationDetails?: MiniAppNotificationDetails | null;
2929
}): Promise<SendFrameNotificationResult> {
3030
if (!notificationDetails) {
3131
notificationDetails = await getUserNotificationDetails(fid);
@@ -37,7 +37,7 @@ export async function sendFrameNotification({
3737
// Define a strict allowlist of full hostnames
3838
const allowedHostnames = ["api.coinbase.com"];
3939
const url = new URL(notificationDetails.url);
40-
40+
4141
// Validate the URL scheme and hostname
4242
if (url.protocol !== "https:" || !allowedHostnames.includes(url.hostname)) {
4343
return { state: "error", error: "Invalid or unsafe notification URL" };

examples/minikit-example/lib/notification.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { FrameNotificationDetails } from "@farcaster/frame-sdk";
1+
import type { MiniAppNotificationDetails } from "@farcaster/frame-sdk";
22
import { redis } from "./redis";
33

44
const notificationServiceKey =
@@ -10,19 +10,19 @@ function getUserNotificationDetailsKey(fid: number): string {
1010

1111
export async function getUserNotificationDetails(
1212
fid: number,
13-
): Promise<FrameNotificationDetails | null> {
13+
): Promise<MiniAppNotificationDetails | null> {
1414
if (!redis) {
1515
return null;
1616
}
1717

18-
return await redis.get<FrameNotificationDetails>(
18+
return await redis.get<MiniAppNotificationDetails>(
1919
getUserNotificationDetailsKey(fid),
2020
);
2121
}
2222

2323
export async function setUserNotificationDetails(
2424
fid: number,
25-
notificationDetails: FrameNotificationDetails,
25+
notificationDetails: MiniAppNotificationDetails,
2626
): Promise<void> {
2727
if (!redis) {
2828
return;

examples/minikit-example/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,17 @@
1111
},
1212
"dependencies": {
1313
"@coinbase/onchainkit": "workspace:*",
14-
"@farcaster/frame-sdk": "^0.0.60",
14+
"@farcaster/frame-sdk": "^0.1.7",
15+
"@tanstack/react-query": "^5",
1516
"@upstash/redis": "^1.34.4",
1617
"next": "^15.3.3",
1718
"react": "^18",
1819
"react-dom": "^18",
19-
"@tanstack/react-query": "^5",
2020
"viem": "^2.27.2",
2121
"wagmi": "^2.14.11"
2222
},
2323
"devDependencies": {
24+
"@farcaster/quick-auth": "^0.0.7",
2425
"@types/node": "^22",
2526
"@types/react": "^18",
2627
"@types/react-dom": "^18",

packages/create-onchain/templates/minikit-basic/lib/notification-client.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import {
2-
FrameNotificationDetails,
2+
MiniAppNotificationDetails,
33
type SendNotificationRequest,
44
sendNotificationResponseSchema,
55
} from "@farcaster/frame-sdk";
@@ -25,7 +25,7 @@ export async function sendFrameNotification({
2525
fid: number;
2626
title: string;
2727
body: string;
28-
notificationDetails?: FrameNotificationDetails | null;
28+
notificationDetails?: MiniAppNotificationDetails | null;
2929
}): Promise<SendFrameNotificationResult> {
3030
if (!notificationDetails) {
3131
notificationDetails = await getUserNotificationDetails(fid);

0 commit comments

Comments
 (0)