Skip to content

Commit f049305

Browse files
committed
feat: start farcaster frames v2 support.
1 parent 6378f37 commit f049305

File tree

5 files changed

+280
-33
lines changed

5 files changed

+280
-33
lines changed

apps/dashboard/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
"@chakra-ui/theme-tools": "^2.1.2",
2828
"@emotion/react": "11.14.0",
2929
"@emotion/styled": "11.14.0",
30+
"@farcaster/frame-core": "^0.1.8",
31+
"@farcaster/frame-sdk": "^0.0.60",
3032
"@hookform/resolvers": "^3.9.1",
3133
"@marsidev/react-turnstile": "^1.1.0",
3234
"@radix-ui/react-accordion": "^1.2.7",
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
"use client";
2+
3+
import type { AddMiniAppResult } from "@farcaster/frame-core/dist/actions/AddMiniApp";
4+
import type { FrameContext } from "@farcaster/frame-core/dist/context";
5+
import { sdk } from "@farcaster/frame-sdk";
6+
import { useQuery } from "@tanstack/react-query";
7+
import {
8+
type ReactNode,
9+
createContext,
10+
useCallback,
11+
useContext,
12+
useState,
13+
} from "react";
14+
15+
interface MiniAppContextType {
16+
isMiniAppReady: boolean;
17+
context: FrameContext | null;
18+
setMiniAppReady: () => void;
19+
addMiniApp: () => Promise<AddMiniAppResult | null>;
20+
}
21+
22+
// eslint-disable-next-line no-restricted-syntax
23+
const MiniAppContext = createContext<MiniAppContextType | undefined>(undefined);
24+
25+
export function MiniAppProvider({
26+
addMiniAppOnLoad,
27+
children,
28+
}: {
29+
addMiniAppOnLoad?: boolean;
30+
children: ReactNode;
31+
}) {
32+
const [context, setContext] = useState<FrameContext | null>(null);
33+
const [isMiniAppReady, setIsMiniAppReady] = useState(false);
34+
35+
const setMiniAppReady = useCallback(async () => {
36+
try {
37+
const context = await sdk.context;
38+
if (context) {
39+
setContext(context as FrameContext);
40+
}
41+
await sdk.actions.ready();
42+
} catch (err) {
43+
console.error("SDK initialization error:", err);
44+
} finally {
45+
setIsMiniAppReady(true);
46+
}
47+
}, []);
48+
49+
useQuery({
50+
queryKey: ["frame-ready"],
51+
queryFn: async () => {
52+
try {
53+
await setMiniAppReady();
54+
return true;
55+
} catch (error) {
56+
console.error("[error] setting mini app ready", error);
57+
return false;
58+
}
59+
},
60+
});
61+
62+
const handleAddMiniApp = useCallback(async () => {
63+
try {
64+
const result = await sdk.actions.addFrame();
65+
if (result) {
66+
return result;
67+
}
68+
return null;
69+
} catch (error) {
70+
console.error("[error] adding frame", error);
71+
return null;
72+
}
73+
}, []);
74+
75+
useQuery({
76+
queryKey: ["frame-add"],
77+
queryFn: async () => {
78+
try {
79+
await handleAddMiniApp();
80+
return true;
81+
} catch (error) {
82+
console.error("[error] adding frame", error);
83+
return false;
84+
}
85+
},
86+
enabled: isMiniAppReady && !context?.client?.added && addMiniAppOnLoad,
87+
});
88+
89+
return (
90+
<MiniAppContext.Provider
91+
value={{
92+
isMiniAppReady,
93+
setMiniAppReady,
94+
addMiniApp: handleAddMiniApp,
95+
context,
96+
}}
97+
>
98+
{children}
99+
</MiniAppContext.Provider>
100+
);
101+
}
102+
103+
export function useMiniApp() {
104+
const context = useContext(MiniAppContext);
105+
if (context === undefined) {
106+
throw new Error("useMiniApp must be used within a MiniAppProvider");
107+
}
108+
return context;
109+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
/**
2+
* Get the farcaster manifest for the frame, generate yours from Warpcast Mobile
3+
* On your phone to Settings > Developer > Domains > insert website hostname > Generate domain manifest
4+
* @returns The farcaster manifest for the frame
5+
*/
6+
export async function getFarcasterManifest() {
7+
let frameName = "Nebula";
8+
let noindex = false;
9+
const appUrl = process.env.NEXT_PUBLIC_URL || "http://nebula.localhost:3000";
10+
if (appUrl.includes("localhost")) {
11+
frameName += " Local";
12+
noindex = true;
13+
} else if (appUrl.includes("ngrok")) {
14+
frameName += " NGROK";
15+
noindex = true;
16+
} else if (appUrl.includes("https://dev.")) {
17+
frameName += " Dev";
18+
noindex = true;
19+
}
20+
return {
21+
accountAssociation: {
22+
header: process.env.NEXT_PUBLIC_FARCASTER_HEADER,
23+
payload: process.env.NEXT_PUBLIC_FARCASTER_PAYLOAD,
24+
signature: process.env.NEXT_PUBLIC_FARCASTER_SIGNATURE,
25+
},
26+
frame: {
27+
version: "1",
28+
name: frameName,
29+
iconUrl: `${appUrl}/images/icon.png`,
30+
homeUrl: appUrl,
31+
imageUrl: `${appUrl}/images/feed.png`,
32+
buttonTitle: "Launch App",
33+
splashImageUrl: `${appUrl}/images/splash.png`,
34+
splashBackgroundColor: "#FFFFFF",
35+
webhookUrl: `${appUrl}/api/webhook`,
36+
// Metadata https://github.com/farcasterxyz/miniapps/discussions/191
37+
subtitle: "AI-powered chat", // 30 characters, no emojis or special characters, short description under app name
38+
description: "AI-powered chat", // 170 characters, no emojis or special characters, promotional message displayed on Mini App Page
39+
primaryCategory: "social",
40+
tags: ["nebula", "ai", "defi", "chat", "social"], // up to 5 tags, filtering/search tags
41+
tagline: "AI-powered chat", // 30 characters, marketing tagline should be punchy and descriptive
42+
ogTitle: `${frameName}`, // 30 characters, app name + short tag, Title case, no emojis
43+
ogDescription: "AI-powered chat", // 100 characters, summarize core benefits in 1-2 lines
44+
screenshotUrls: [
45+
// 1284 x 2778, visual previews of the app, max 3 screenshots
46+
`${appUrl}/images/feed.png`,
47+
],
48+
heroImageUrl: `${appUrl}/images/feed.png`, // 1200 x 630px (1.91:1), promotional display image on top of the mini app store
49+
ogImageUrl: `${appUrl}/images/feed.png`, // 1200 x 630px (1.91:1), promotional image, same as app hero image
50+
noindex: noindex,
51+
},
52+
};
53+
}

apps/dashboard/src/app/nebula-app/providers.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,27 @@ import { useMemo } from "react";
77
import { Toaster } from "sonner";
88
import { ThirdwebProvider, useActiveAccount } from "thirdweb/react";
99
import { NebulaConnectWallet } from "./(app)/components/NebulaConnectButton";
10+
import { MiniAppProvider } from "./_farcaster/miniapp-context";
1011

1112
const queryClient = new QueryClient();
1213

1314
export function NebulaProviders(props: { children: React.ReactNode }) {
1415
return (
1516
<QueryClientProvider client={queryClient}>
1617
<ThirdwebProvider>
17-
<ThemeProvider
18-
attribute="class"
19-
disableTransitionOnChange
20-
enableSystem={false}
21-
defaultTheme="light"
22-
>
23-
<ToasterSetup />
24-
<SanctionedAddressesChecker>
25-
{props.children}
26-
</SanctionedAddressesChecker>
27-
</ThemeProvider>
18+
<MiniAppProvider>
19+
<ThemeProvider
20+
attribute="class"
21+
disableTransitionOnChange
22+
enableSystem={false}
23+
defaultTheme="light"
24+
>
25+
<ToasterSetup />
26+
<SanctionedAddressesChecker>
27+
{props.children}
28+
</SanctionedAddressesChecker>
29+
</ThemeProvider>
30+
</MiniAppProvider>
2831
</ThirdwebProvider>
2932
</QueryClientProvider>
3033
);

0 commit comments

Comments
 (0)