Skip to content
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-hands-sing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"create-onchain": patch
---

Update @farcaster/frame-sdk dependency
5 changes: 5 additions & 0 deletions .changeset/green-socks-call.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@coinbase/onchainkit": patch
---

Update @farcaster/frame-sdk dependency
6 changes: 5 additions & 1 deletion examples/minikit-example/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
NEXT_PUBLIC_ONCHAINKIT_PROJECT_NAME=minikit-example
NEXT_PUBLIC_URL=
NEXT_PUBLIC_ICON_URL=$NEXT_PUBLIC_URL/logo.png
NEXT_PUBLIC_ONCHAINKIT_API_KEY=""
NEXT_PUBLIC_ONCHAINKIT_API_KEY=

# Frame metadata

Expand All @@ -23,6 +23,10 @@ NEXT_PUBLIC_APP_OG_TITLE=minikit-example
NEXT_PUBLIC_APP_OG_DESCRIPTION=
NEXT_PUBLIC_APP_OG_IMAGE=$NEXT_PUBLIC_URL/hero.png

# Neynar

NEYNAR_API_KEY=

# Redis config

REDIS_URL=
Expand Down
73 changes: 73 additions & 0 deletions examples/minikit-example/app/api/me/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { Errors, createClient } from "@farcaster/quick-auth";
import { NextRequest, NextResponse } from "next/server";

const client = createClient();

function getUrlHost() {
let urlValue: string;

if (process.env.VERCEL_ENV === "production") {
urlValue = process.env.NEXT_PUBLIC_URL!;
} else if (process.env.VERCEL_URL) {
urlValue = `https://${process.env.VERCEL_URL}`;
} else {
urlValue = "http://localhost:3000";
}

const url = new URL(urlValue);

return url.host;
}

export async function GET(request: NextRequest) {
// Because we're fetching this endpoint via `sdk.quickAuth.fetch`,
// if we're in a mini app, the request will include the necessary `Authorization` header.
const authorization = request.headers.get("Authorization");

// Here we ensure that we have a valid token.
if (!authorization || !authorization.startsWith("Bearer ")) {
return NextResponse.json({ message: "Missing token" }, { status: 401 });
}

try {
// Now we verify the token. `domain` must match the domain of the request.
// In our case, we're using the `getUrlHost` function to get the domain of the request
// based on the Vercel environment. This will vary depending on your hosting provider.
const payload = await client.verifyJwt({
token: authorization.split(" ")[1] as string,
domain: getUrlHost(),
});

// If the token was valid, `payload.sub` will be the user's Farcaster ID.
const userFid = payload.sub;

// And now we can use that FID to do whatever we want.
// In this example, we're going to get the user's info from Neynar and return it.
const userInfoResult = await fetch(
`https://api.neynar.com/v2/farcaster/user/bulk?fids=${userFid}`,
{
headers: {
"x-api-key": process.env.NEYNAR_API_KEY || "",
},
},
).then((res) => res.json());

const userInfo = userInfoResult?.users?.[0];

if (!userInfo) {
return NextResponse.json({ message: "User not found" }, { status: 404 });
}

return NextResponse.json(userInfo);
} catch (e) {
if (e instanceof Errors.InvalidTokenError) {
return NextResponse.json({ message: "Invalid token" }, { status: 401 });
}

if (e instanceof Error) {
return NextResponse.json({ message: e.message }, { status: 500 });
}

throw e;
}
}
105 changes: 105 additions & 0 deletions examples/minikit-example/app/components/UserInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { useIsInMiniApp } from "@coinbase/onchainkit/minikit";
import sdk from "@farcaster/frame-sdk";
import { useQuery } from "@tanstack/react-query";

function useUserInfo() {
const { isInMiniApp } = useIsInMiniApp();

return useQuery({
queryKey: ["useQuickAuth", isInMiniApp],
queryFn: async () => {
// If we're in a mini app context, all we have to do to make an authenticated
// request is to use `sdk.quickAuth.fetch`. This will automatically include the
// necessary `Authorization` header for the backend to verify.
const result = await sdk.quickAuth.fetch("/api/me");

const userInfo = await result.json();
return {
displayName: userInfo.display_name,
pfpUrl: userInfo.pfp_url,
bio: userInfo.profile?.bio?.text,
followerCount: userInfo.follower_count,
followingCount: userInfo.following_count,
};
},
enabled: isInMiniApp,
});
}

export function UserInfo() {
const { data, isLoading, error } = useUserInfo();

if (isLoading) {
return (
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-4 animate-pulse">
<div className="flex items-center space-x-4">
<div className="w-16 h-16 bg-[var(--app-gray)] rounded-full"></div>
<div className="space-y-2 flex-1">
<div className="h-5 bg-[var(--app-gray)] rounded w-32"></div>
<div className="h-4 bg-[var(--app-gray)] rounded w-24"></div>
</div>
</div>
</div>
);
}

if (error || !data) {
return (
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-4">
<p className="text-[var(--app-foreground-muted)] text-center">
{error ? "Failed to load user info" : "No user info available"}
</p>
</div>
);
}

return (
<div className="bg-[var(--app-card-bg)] border border-[var(--app-card-border)] rounded-lg p-6 backdrop-blur-sm">
<div className="flex items-start space-x-4">
{/* Profile Picture */}
{data.pfpUrl && (
<img
src={data.pfpUrl}
alt={`${data.displayName}'s profile picture`}
className="w-16 h-16 rounded-full object-cover border-2 border-[var(--app-card-border)]"
/>
)}

{/* User Info */}
<div className="flex-1 min-w-0">
{/* Display Name */}
<h2 className="text-xl font-bold text-[var(--app-foreground)] mb-2 truncate">
{data.displayName}
</h2>

{/* Bio */}
{data.bio && (
<p className="text-[var(--app-foreground-muted)] text-sm mb-3 leading-relaxed">
{data.bio}
</p>
)}

{/* Follower Stats */}
<div className="flex space-x-6 text-sm">
<div className="flex flex-col items-center">
<span className="font-semibold text-[var(--app-foreground)]">
{data.followerCount?.toLocaleString() || "0"}
</span>
<span className="text-[var(--app-foreground-muted)]">
Followers
</span>
</div>
<div className="flex flex-col items-center">
<span className="font-semibold text-[var(--app-foreground)]">
{data.followingCount?.toLocaleString() || "0"}
</span>
<span className="text-[var(--app-foreground-muted)]">
Following
</span>
</div>
</div>
</div>
</div>
</div>
);
}
2 changes: 2 additions & 0 deletions examples/minikit-example/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { AddFrame } from "./actions/AddFrame";
import { ComposeCast } from "./actions/ComposeCast";
import { ViewCast } from "./actions/ViewCast";
import { CloseFrame } from "./actions/CloseFrame";
import { UserInfo } from "./components/UserInfo";

export default function App() {
const { setFrameReady, isFrameReady } = useMiniKit();
Expand Down Expand Up @@ -68,6 +69,7 @@ export default function App() {

<div className="flex flex-col gap-3 justify-center items-stretch max-w-md mx-auto">
<IsInMiniApp />
<UserInfo />
<AddFrame />
<ComposeCast />
<ViewCast />
Expand Down
6 changes: 3 additions & 3 deletions examples/minikit-example/lib/notification-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {
FrameNotificationDetails,
MiniAppNotificationDetails,
type SendNotificationRequest,
sendNotificationResponseSchema,
} from "@farcaster/frame-sdk";
Expand All @@ -25,7 +25,7 @@ export async function sendFrameNotification({
fid: number;
title: string;
body: string;
notificationDetails?: FrameNotificationDetails | null;
notificationDetails?: MiniAppNotificationDetails | null;
}): Promise<SendFrameNotificationResult> {
if (!notificationDetails) {
notificationDetails = await getUserNotificationDetails(fid);
Expand All @@ -37,7 +37,7 @@ export async function sendFrameNotification({
// Define a strict allowlist of full hostnames
const allowedHostnames = ["api.coinbase.com"];
const url = new URL(notificationDetails.url);

// Validate the URL scheme and hostname
if (url.protocol !== "https:" || !allowedHostnames.includes(url.hostname)) {
return { state: "error", error: "Invalid or unsafe notification URL" };
Expand Down
8 changes: 4 additions & 4 deletions examples/minikit-example/lib/notification.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { FrameNotificationDetails } from "@farcaster/frame-sdk";
import type { MiniAppNotificationDetails } from "@farcaster/frame-sdk";
import { redis } from "./redis";

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

export async function getUserNotificationDetails(
fid: number,
): Promise<FrameNotificationDetails | null> {
): Promise<MiniAppNotificationDetails | null> {
if (!redis) {
return null;
}

return await redis.get<FrameNotificationDetails>(
return await redis.get<MiniAppNotificationDetails>(
getUserNotificationDetailsKey(fid),
);
}

export async function setUserNotificationDetails(
fid: number,
notificationDetails: FrameNotificationDetails,
notificationDetails: MiniAppNotificationDetails,
): Promise<void> {
if (!redis) {
return;
Expand Down
5 changes: 3 additions & 2 deletions examples/minikit-example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,16 +11,17 @@
},
"dependencies": {
"@coinbase/onchainkit": "workspace:*",
"@farcaster/frame-sdk": "^0.0.60",
"@farcaster/frame-sdk": "^0.1.7",
"@tanstack/react-query": "^5",
"@upstash/redis": "^1.34.4",
"next": "^15.3.3",
"react": "^18",
"react-dom": "^18",
"@tanstack/react-query": "^5",
"viem": "^2.27.2",
"wagmi": "^2.14.11"
},
"devDependencies": {
"@farcaster/quick-auth": "^0.0.7",
"@types/node": "^22",
"@types/react": "^18",
"@types/react-dom": "^18",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@coinbase/onchainkit": "latest",
"@farcaster/frame-sdk": "^0.0.60",
"@farcaster/frame-sdk": "^0.1.7",
"@upstash/redis": "^1.34.4",
"next": "^15.3.3",
"react": "^18",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
},
"dependencies": {
"@coinbase/onchainkit": "latest",
"@farcaster/frame-sdk": "^0.0.60",
"@farcaster/frame-sdk": "^0.1.7",
"@upstash/redis": "^1.34.4",
"next": "^15.3.3",
"react": "^18",
Expand Down
2 changes: 1 addition & 1 deletion packages/onchainkit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
"react-dom": "^18 || ^19"
},
"dependencies": {
"@farcaster/frame-sdk": "^0.0.60",
"@farcaster/frame-sdk": "^0.1.7",
"@farcaster/frame-wagmi-connector": "^0.0.53",
"@tanstack/react-query": "^5",
"@wagmi/core": "^2.16.7",
Expand Down
18 changes: 11 additions & 7 deletions packages/onchainkit/src/minikit/MiniKitProvider.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('MiniKitProvider', () => {
added: false,
safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 },
},
}) as unknown as Promise<Context.FrameContext>;
}) as unknown as Promise<Context.MiniAppContext>;
});

afterEach(() => {
Expand Down Expand Up @@ -161,8 +161,12 @@ describe('MiniKitProvider', () => {

await act(() => Promise.resolve());

expect(sdk.on).toHaveBeenCalledWith('frameAdded', expect.any(Function));
expect(sdk.on).toHaveBeenCalledWith('frameRemoved', expect.any(Function));
expect(sdk.on).toHaveBeenCalledWith('miniAppAdded', expect.any(Function));
expect(sdk.on).toHaveBeenCalledWith(
'miniAppAddRejected',
expect.any(Function),
);
expect(sdk.on).toHaveBeenCalledWith('miniAppRemoved', expect.any(Function));
expect(sdk.on).toHaveBeenCalledWith(
'notificationsEnabled',
expect.any(Function),
Expand Down Expand Up @@ -217,7 +221,7 @@ describe('MiniKitProvider', () => {
};

act(() => {
sdk.emit('frameAdded', {
sdk.emit('miniAppAdded', {
notificationDetails,
});
});
Expand All @@ -228,7 +232,7 @@ describe('MiniKitProvider', () => {
expect(contextValue?.context?.client.added).toBe(true);

act(() => {
sdk.emit('frameRemoved');
sdk.emit('miniAppRemoved');
});

expect(contextValue?.context?.client.notificationDetails).toBeUndefined();
Expand All @@ -252,12 +256,12 @@ describe('MiniKitProvider', () => {

await act(() => Promise.resolve());

sdk.emit('frameAddRejected', {
sdk.emit('miniAppAddRejected', {
reason: 'invalid_domain_manifest',
});

expect(consoleErrorSpy).toHaveBeenCalledWith(
'Frame add rejected',
'Mini app add rejected',
'invalid_domain_manifest',
);
});
Expand Down
Loading
Loading