refactor: update frame to mini app and fix dark mode

This commit is contained in:
veganbeef 2025-06-16 12:59:50 -07:00
parent cfe51f067c
commit 4831308983
No known key found for this signature in database
16 changed files with 95 additions and 94 deletions

View File

@ -1,4 +1,4 @@
# Farcaster Mini Apps (formerly Frames) Quickstart by Neynar 🪐 # Farcaster Mini Apps (formerly Frames v2) Quickstart by Neynar 🪐
A Farcaster Mini Apps quickstart npx script. A Farcaster Mini Apps quickstart npx script.

View File

@ -166,14 +166,14 @@ export async function init() {
break; break;
} }
const defaultFrameName = (neynarAppName && !neynarAppName.toLowerCase().includes('demo')) ? neynarAppName : undefined; const defaultMiniAppName = (neynarAppName && !neynarAppName.toLowerCase().includes('demo')) ? neynarAppName : undefined;
const answers = await inquirer.prompt([ const answers = await inquirer.prompt([
{ {
type: 'input', type: 'input',
name: 'projectName', name: 'projectName',
message: 'What is the name of your mini app?', message: 'What is the name of your mini app?',
default: defaultFrameName, default: defaultMiniAppName,
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Project name cannot be empty'; return 'Project name cannot be empty';
@ -400,11 +400,11 @@ export async function init() {
fs.writeFileSync(envPath, envExampleContent); fs.writeFileSync(envPath, envExampleContent);
// Append all remaining environment variables // Append all remaining environment variables
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_NAME="${answers.projectName}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_MINI_APP_NAME="${answers.projectName}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_DESCRIPTION="${answers.description}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_MINI_APP_DESCRIPTION="${answers.description}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_PRIMARY_CATEGORY="${answers.primaryCategory}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${answers.primaryCategory}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_TAGS="${answers.tags.join(',')}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_MINI_APP_TAGS="${answers.tags.join(',')}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_FRAME_BUTTON_TEXT="${answers.buttonText}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${answers.buttonText}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_ANALYTICS_ENABLED="${answers.enableAnalytics}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_ANALYTICS_ENABLED="${answers.enableAnalytics}"`);
fs.appendFileSync(envPath, `\nNEXT_PUBLIC_USE_WALLET="${answers.useWallet}"`); fs.appendFileSync(envPath, `\nNEXT_PUBLIC_USE_WALLET="${answers.useWallet}"`);

View File

@ -1,6 +1,6 @@
{ {
"name": "@neynar/create-farcaster-mini-app", "name": "@neynar/create-farcaster-mini-app",
"version": "1.3.2", "version": "1.4.0",
"type": "module", "type": "module",
"private": false, "private": false,
"access": "public", "access": "public",

View File

@ -161,7 +161,7 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase
}); });
const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url');
const tags = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(',');
return { return {
accountAssociation: { accountAssociation: {
@ -171,16 +171,16 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase
}, },
frame: { frame: {
version: "1", version: "1",
name: process.env.NEXT_PUBLIC_FRAME_NAME, name: process.env.NEXT_PUBLIC_MINI_APP_NAME,
iconUrl: `https://${domain}/icon.png`, iconUrl: `https://${domain}/icon.png`,
homeUrl: `https://${domain}`, homeUrl: `https://${domain}`,
imageUrl: `https://${domain}/api/opengraph-image`, imageUrl: `https://${domain}/api/opengraph-image`,
buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT, buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT,
splashImageUrl: `https://${domain}/splash.png`, splashImageUrl: `https://${domain}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",
webhookUrl, webhookUrl,
description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION, description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION,
primaryCategory: process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY, primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY,
tags, tags,
}, },
}; };
@ -217,7 +217,7 @@ async function main() {
type: 'input', type: 'input',
name: 'frameName', name: 'frameName',
message: 'Enter the name for your mini app (e.g., My Cool Mini App):', message: 'Enter the name for your mini app (e.g., My Cool Mini App):',
default: process.env.NEXT_PUBLIC_FRAME_NAME, default: process.env.NEXT_PUBLIC_MINI_APP_NAME,
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Mini app name cannot be empty'; return 'Mini app name cannot be empty';
@ -233,7 +233,7 @@ async function main() {
type: 'input', type: 'input',
name: 'buttonText', name: 'buttonText',
message: 'Enter the text for your mini app button:', message: 'Enter the text for your mini app button:',
default: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT || 'Launch Mini App', default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App',
validate: (input) => { validate: (input) => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Button text cannot be empty'; return 'Button text cannot be empty';
@ -355,12 +355,12 @@ async function main() {
// Base URL // Base URL
`NEXT_PUBLIC_URL=https://${domain}`, `NEXT_PUBLIC_URL=https://${domain}`,
// Frame metadata // Mini app metadata
`NEXT_PUBLIC_FRAME_NAME="${frameName}"`, `NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`,
`NEXT_PUBLIC_FRAME_DESCRIPTION="${process.env.NEXT_PUBLIC_FRAME_DESCRIPTION || ''}"`, `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || ''}"`,
`NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY="${process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY || ''}"`, `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || ''}"`,
`NEXT_PUBLIC_FRAME_TAGS="${process.env.NEXT_PUBLIC_FRAME_TAGS || ''}"`, `NEXT_PUBLIC_MINI_APP_TAGS="${process.env.NEXT_PUBLIC_MINI_APP_TAGS || ''}"`,
`NEXT_PUBLIC_FRAME_BUTTON_TEXT="${buttonText}"`, `NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`,
// Analytics // Analytics
`NEXT_PUBLIC_ANALYTICS_ENABLED="${process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'}"`, `NEXT_PUBLIC_ANALYTICS_ENABLED="${process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'}"`,
@ -379,8 +379,8 @@ async function main() {
`NEXTAUTH_SECRET="${process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex')}"`, `NEXTAUTH_SECRET="${process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex')}"`,
`NEXTAUTH_URL="https://${domain}"`, `NEXTAUTH_URL="https://${domain}"`,
// Frame manifest with signature // Mini app manifest with signature
`FRAME_METADATA=${JSON.stringify(metadata)}`, `MINI_APP_METADATA=${JSON.stringify(metadata)}`,
]; ];
// Filter out empty values and join with newlines // Filter out empty values and join with newlines

View File

@ -72,7 +72,7 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase
}); });
const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url'); const encodedSignature = Buffer.from(signature, 'utf-8').toString('base64url');
const tags = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(',');
return { return {
accountAssociation: { accountAssociation: {
@ -82,16 +82,16 @@ async function generateFarcasterMetadata(domain, fid, accountAddress, seedPhrase
}, },
frame: { frame: {
version: "1", version: "1",
name: process.env.NEXT_PUBLIC_FRAME_NAME, name: process.env.NEXT_PUBLIC_MINI_APP_NAME,
iconUrl: `https://${trimmedDomain}/icon.png`, iconUrl: `https://${trimmedDomain}/icon.png`,
homeUrl: `https://${trimmedDomain}`, homeUrl: `https://${trimmedDomain}`,
imageUrl: `https://${trimmedDomain}/api/opengraph-image`, imageUrl: `https://${trimmedDomain}/api/opengraph-image`,
buttonTitle: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT, buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT,
splashImageUrl: `https://${trimmedDomain}/splash.png`, splashImageUrl: `https://${trimmedDomain}/splash.png`,
splashBackgroundColor: "#f7f7f7", splashBackgroundColor: "#f7f7f7",
webhookUrl: webhookUrl?.trim(), webhookUrl: webhookUrl?.trim(),
description: process.env.NEXT_PUBLIC_FRAME_DESCRIPTION, description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION,
primaryCategory: process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY, primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY,
tags, tags,
}, },
}; };
@ -116,11 +116,11 @@ async function loadEnvLocal() {
// Define allowed variables to load from .env.local // Define allowed variables to load from .env.local
const allowedVars = [ const allowedVars = [
'SEED_PHRASE', 'SEED_PHRASE',
'NEXT_PUBLIC_FRAME_NAME', 'NEXT_PUBLIC_MINI_APP_NAME',
'NEXT_PUBLIC_FRAME_DESCRIPTION', 'NEXT_PUBLIC_MINI_APP_DESCRIPTION',
'NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY', 'NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY',
'NEXT_PUBLIC_FRAME_TAGS', 'NEXT_PUBLIC_MINI_APP_TAGS',
'NEXT_PUBLIC_FRAME_BUTTON_TEXT', 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT',
'NEXT_PUBLIC_ANALYTICS_ENABLED', 'NEXT_PUBLIC_ANALYTICS_ENABLED',
'NEYNAR_API_KEY', 'NEYNAR_API_KEY',
'NEYNAR_CLIENT_ID' 'NEYNAR_CLIENT_ID'
@ -161,15 +161,15 @@ async function checkRequiredEnvVars() {
const requiredVars = [ const requiredVars = [
{ {
name: 'NEXT_PUBLIC_FRAME_NAME', name: 'NEXT_PUBLIC_MINI_APP_NAME',
message: 'Enter the name for your frame (e.g., My Cool Mini App):', message: 'Enter the name for your frame (e.g., My Cool Mini App):',
default: process.env.NEXT_PUBLIC_FRAME_NAME, default: process.env.NEXT_PUBLIC_MINI_APP_NAME,
validate: input => input.trim() !== '' || 'Mini app name cannot be empty' validate: input => input.trim() !== '' || 'Mini app name cannot be empty'
}, },
{ {
name: 'NEXT_PUBLIC_FRAME_BUTTON_TEXT', name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT',
message: 'Enter the text for your frame button:', message: 'Enter the text for your frame button:',
default: process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT ?? 'Launch Mini App', default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App',
validate: input => input.trim() !== '' || 'Button text cannot be empty' validate: input => input.trim() !== '' || 'Button text cannot be empty'
} }
]; ];
@ -340,7 +340,7 @@ async function setVercelEnvVar(key, value, projectRoot) {
// Ignore errors from removal (var might not exist) // Ignore errors from removal (var might not exist)
} }
// For complex objects like frameMetadata, use a temporary file approach // For complex objects like miniAppMetadata, use a temporary file approach
if (typeof value === 'object') { if (typeof value === 'object') {
const tempFilePath = path.join(projectRoot, `${key}_temp.json`); const tempFilePath = path.join(projectRoot, `${key}_temp.json`);
// Write the value to a temporary file with proper JSON formatting // Write the value to a temporary file with proper JSON formatting
@ -432,11 +432,11 @@ async function deployToVercel(useGitHub = false) {
} }
} }
// Generate frame metadata if we have a seed phrase // Generate mini app metadata if we have a seed phrase
let frameMetadata; let miniAppMetadata;
let fid; let fid;
if (process.env.SEED_PHRASE) { if (process.env.SEED_PHRASE) {
console.log('\n🔨 Generating frame metadata...'); console.log('\n🔨 Generating mini app metadata...');
const accountAddress = await validateSeedPhrase(process.env.SEED_PHRASE); const accountAddress = await validateSeedPhrase(process.env.SEED_PHRASE);
fid = await lookupFidByCustodyAddress(accountAddress, process.env.NEYNAR_API_KEY ?? 'FARCASTER_V2_FRAMES_DEMO'); fid = await lookupFidByCustodyAddress(accountAddress, process.env.NEYNAR_API_KEY ?? 'FARCASTER_V2_FRAMES_DEMO');
@ -445,8 +445,8 @@ async function deployToVercel(useGitHub = false) {
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `https://${domain}/api/webhook`; : `https://${domain}/api/webhook`;
frameMetadata = await generateFarcasterMetadata(domain, fid, accountAddress, process.env.SEED_PHRASE, webhookUrl); miniAppMetadata = await generateFarcasterMetadata(domain, fid, accountAddress, process.env.SEED_PHRASE, webhookUrl);
console.log('✅ Frame metadata generated and signed'); console.log('✅ Mini app metadata generated and signed');
} }
// Prepare environment variables // Prepare environment variables
@ -462,8 +462,8 @@ async function deployToVercel(useGitHub = false) {
...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }), ...(process.env.NEYNAR_API_KEY && { NEYNAR_API_KEY: process.env.NEYNAR_API_KEY }),
...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }), ...(process.env.NEYNAR_CLIENT_ID && { NEYNAR_CLIENT_ID: process.env.NEYNAR_CLIENT_ID }),
// Frame metadata - don't stringify here // Mini app metadata - don't stringify here
...(frameMetadata && { FRAME_METADATA: frameMetadata }), ...(miniAppMetadata && { MINI_APP_METADATA: miniAppMetadata }),
// Public vars // Public vars
...Object.fromEntries( ...Object.fromEntries(
@ -539,10 +539,10 @@ async function deployToVercel(useGitHub = false) {
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `https://${actualDomain}/api/webhook`; : `https://${actualDomain}/api/webhook`;
if (frameMetadata) { if (miniAppMetadata) {
frameMetadata = await generateFarcasterMetadata(actualDomain, fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl); miniAppMetadata = await generateFarcasterMetadata(actualDomain, fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, webhookUrl);
// Update FRAME_METADATA env var using the new function // Update MINI_APP_METADATA env var using the new function
await setVercelEnvVar('FRAME_METADATA', frameMetadata, projectRoot); await setVercelEnvVar('MINI_APP_METADATA', miniAppMetadata, projectRoot);
} }
// Update NEXTAUTH_URL // Update NEXTAUTH_URL

View File

@ -81,7 +81,7 @@ async function startDev() {
} }
const useTunnel = process.env.USE_TUNNEL === 'true'; const useTunnel = process.env.USE_TUNNEL === 'true';
let frameUrl; let miniAppUrl;
if (useTunnel) { if (useTunnel) {
// Start localtunnel and get URL // Start localtunnel and get URL
@ -93,7 +93,7 @@ async function startDev() {
console.error('Error getting IP address:', error); console.error('Error getting IP address:', error);
} }
frameUrl = tunnel.url; miniAppUrl = tunnel.url;
console.log(` console.log(`
🌐 Local tunnel URL: ${tunnel.url} 🌐 Local tunnel URL: ${tunnel.url}
@ -117,12 +117,12 @@ async function startDev() {
5. Click "Preview" (note that it may take ~10 seconds to load) 5. Click "Preview" (note that it may take ~10 seconds to load)
`); `);
} else { } else {
frameUrl = 'http://localhost:3000'; miniAppUrl = 'http://localhost:3000';
console.log(` console.log(`
💻 To test your mini app: 💻 To test your mini app:
1. Open the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers 1. Open the Warpcast Mini App Developer Tools: https://warpcast.com/~/developers
2. Scroll down to the "Preview Mini App" tool 2. Scroll down to the "Preview Mini App" tool
3. Enter this URL: ${frameUrl} 3. Enter this URL: ${miniAppUrl}
4. Click "Preview" to test your mini app (note that it may take ~5 seconds to load the first time) 4. Click "Preview" to test your mini app (note that it may take ~5 seconds to load the first time)
`); `);
} }
@ -132,7 +132,7 @@ async function startDev() {
nextDev = spawn(nextBin, ['dev'], { nextDev = spawn(nextBin, ['dev'], {
stdio: 'inherit', stdio: 'inherit',
env: { ...process.env, NEXT_PUBLIC_URL: frameUrl, NEXTAUTH_URL: frameUrl }, env: { ...process.env, NEXT_PUBLIC_URL: miniAppUrl, NEXTAUTH_URL: miniAppUrl },
cwd: projectRoot, cwd: projectRoot,
shell: process.platform === 'win32' // Add shell option for Windows shell: process.platform === 'win32' // Add shell option for Windows
}); });

View File

@ -2,8 +2,8 @@ import { notificationDetailsSchema } from "@farcaster/frame-sdk";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
import { z } from "zod"; import { z } from "zod";
import { setUserNotificationDetails } from "~/lib/kv"; import { setUserNotificationDetails } from "~/lib/kv";
import { sendFrameNotification } from "~/lib/notifs"; import { sendMiniAppNotification } from "~/lib/notifs";
import { sendNeynarFrameNotification } from "~/lib/neynar"; import { sendNeynarMiniAppNotification } from "~/lib/neynar";
const requestSchema = z.object({ const requestSchema = z.object({
fid: z.number(), fid: z.number(),
@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
} }
// Use appropriate notification function based on Neynar status // Use appropriate notification function based on Neynar status
const sendNotification = neynarEnabled ? sendNeynarFrameNotification : sendFrameNotification; const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification;
const sendResult = await sendNotification({ const sendResult = await sendNotification({
fid: Number(requestBody.data.fid), fid: Number(requestBody.data.fid),
title: "Test notification", title: "Test notification",

View File

@ -4,11 +4,12 @@ import {
verifyAppKeyWithNeynar, verifyAppKeyWithNeynar,
} from "@farcaster/frame-node"; } from "@farcaster/frame-node";
import { NextRequest } from "next/server"; import { NextRequest } from "next/server";
import { APP_NAME } from "~/lib/constants";
import { import {
deleteUserNotificationDetails, deleteUserNotificationDetails,
setUserNotificationDetails, setUserNotificationDetails,
} from "~/lib/kv"; } from "~/lib/kv";
import { sendFrameNotification } from "~/lib/notifs"; import { sendMiniAppNotification } from "~/lib/notifs";
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to handle webhooks here // If Neynar is enabled, we don't need to handle webhooks here
@ -58,10 +59,10 @@ export async function POST(request: NextRequest) {
case "frame_added": case "frame_added":
if (event.notificationDetails) { if (event.notificationDetails) {
await setUserNotificationDetails(fid, event.notificationDetails); await setUserNotificationDetails(fid, event.notificationDetails);
await sendFrameNotification({ await sendMiniAppNotification({
fid, fid,
title: "Welcome to Frames v2", title: `Welcome to ${APP_NAME}`,
body: "Frame is now added to your client", body: "Mini app is now added to your client",
}); });
} else { } else {
await deleteUserNotificationDetails(fid); await deleteUserNotificationDetails(fid);
@ -74,9 +75,9 @@ export async function POST(request: NextRequest) {
case "notifications_enabled": case "notifications_enabled":
await setUserNotificationDetails(fid, event.notificationDetails); await setUserNotificationDetails(fid, event.notificationDetails);
await sendFrameNotification({ await sendMiniAppNotification({
fid, fid,
title: "Ding ding ding", title: `Welcome to ${APP_NAME}`,
body: "Notifications are now enabled", body: "Notifications are now enabled",
}); });
break; break;

View File

@ -1,7 +1,7 @@
import { Metadata } from "next"; import { Metadata } from "next";
import App from "./app"; import App from "./app";
import { APP_NAME, APP_DESCRIPTION, APP_OG_IMAGE_URL } from "~/lib/constants"; import { APP_NAME, APP_DESCRIPTION, APP_OG_IMAGE_URL } from "~/lib/constants";
import { getFrameEmbedMetadata } from "~/lib/utils"; import { getMiniAppEmbedMetadata } from "~/lib/utils";
export const revalidate = 300; export const revalidate = 300;
@ -14,7 +14,7 @@ export async function generateMetadata(): Promise<Metadata> {
images: [APP_OG_IMAGE_URL], images: [APP_OG_IMAGE_URL],
}, },
other: { other: {
"fc:frame": JSON.stringify(getFrameEmbedMetadata()), "fc:frame": JSON.stringify(getMiniAppEmbedMetadata()),
}, },
}; };
} }

View File

@ -1,7 +1,7 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { APP_URL, APP_NAME, APP_DESCRIPTION } from "~/lib/constants"; import { APP_URL, APP_NAME, APP_DESCRIPTION } from "~/lib/constants";
import { getFrameEmbedMetadata } from "~/lib/utils"; import { getMiniAppEmbedMetadata } from "~/lib/utils";
export const revalidate = 300; export const revalidate = 300;
// This is an example of how to generate a dynamically generated share page based on fid: // This is an example of how to generate a dynamically generated share page based on fid:
@ -23,7 +23,7 @@ export async function generateMetadata({
images: [imageUrl], images: [imageUrl],
}, },
other: { other: {
"fc:frame": JSON.stringify(getFrameEmbedMetadata(imageUrl)), "fc:frame": JSON.stringify(getMiniAppEmbedMetadata(imageUrl)),
}, },
}; };
} }

View File

@ -44,7 +44,7 @@ interface NeynarUser {
} }
export default function Demo( export default function Demo(
{ title }: { title?: string } = { title: "Frames v2 Demo" } { title }: { title?: string } = { title: "Neynar Starter Kit" }
) { ) {
const { const {
isSDKLoaded, isSDKLoaded,
@ -539,7 +539,7 @@ function SignEvmMessage() {
}); });
} }
signMessage({ message: "Hello from Frames v2!" }); signMessage({ message: `Hello from ${APP_NAME}!` });
}, [connectAsync, isConnected, signMessage]); }, [connectAsync, isConnected, signMessage]);
return ( return (

View File

@ -1,13 +1,13 @@
export const APP_URL = process.env.NEXT_PUBLIC_URL!; export const APP_URL = process.env.NEXT_PUBLIC_URL!;
export const APP_NAME = process.env.NEXT_PUBLIC_FRAME_NAME; export const APP_NAME = process.env.NEXT_PUBLIC_MINI_APP_NAME;
export const APP_DESCRIPTION = process.env.NEXT_PUBLIC_FRAME_DESCRIPTION; export const APP_DESCRIPTION = process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION;
export const APP_PRIMARY_CATEGORY = process.env.NEXT_PUBLIC_FRAME_PRIMARY_CATEGORY; export const APP_PRIMARY_CATEGORY = process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY;
export const APP_TAGS = process.env.NEXT_PUBLIC_FRAME_TAGS?.split(','); export const APP_TAGS = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(',');
export const APP_ICON_URL = `${APP_URL}/icon.png`; export const APP_ICON_URL = `${APP_URL}/icon.png`;
export const APP_OG_IMAGE_URL = `${APP_URL}/api/opengraph-image`; export const APP_OG_IMAGE_URL = `${APP_URL}/api/opengraph-image`;
export const APP_SPLASH_URL = `${APP_URL}/splash.png`; export const APP_SPLASH_URL = `${APP_URL}/splash.png`;
export const APP_SPLASH_BACKGROUND_COLOR = "#f7f7f7"; export const APP_SPLASH_BACKGROUND_COLOR = "#f7f7f7";
export const APP_BUTTON_TEXT = process.env.NEXT_PUBLIC_FRAME_BUTTON_TEXT; export const APP_BUTTON_TEXT = process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT;
export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID export const APP_WEBHOOK_URL = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID
? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event` ? `https://api.neynar.com/f/app/${process.env.NEYNAR_CLIENT_ID}/event`
: `${APP_URL}/api/webhook`; : `${APP_URL}/api/webhook`;

View File

@ -31,7 +31,7 @@ export async function getNeynarUser(fid: number): Promise<User | null> {
} }
} }
type SendFrameNotificationResult = type SendMiniAppNotificationResult =
| { | {
state: "error"; state: "error";
error: unknown; error: unknown;
@ -40,7 +40,7 @@ type SendFrameNotificationResult =
| { state: "rate_limit" } | { state: "rate_limit" }
| { state: "success" }; | { state: "success" };
export async function sendNeynarFrameNotification({ export async function sendNeynarMiniAppNotification({
fid, fid,
title, title,
body, body,
@ -48,7 +48,7 @@ export async function sendNeynarFrameNotification({
fid: number; fid: number;
title: string; title: string;
body: string; body: string;
}): Promise<SendFrameNotificationResult> { }): Promise<SendMiniAppNotificationResult> {
try { try {
const client = getNeynarClient(); const client = getNeynarClient();
const targetFids = [fid]; const targetFids = [fid];

View File

@ -5,7 +5,7 @@ import {
import { getUserNotificationDetails } from "~/lib/kv"; import { getUserNotificationDetails } from "~/lib/kv";
import { APP_URL } from "./constants"; import { APP_URL } from "./constants";
type SendFrameNotificationResult = type SendMiniAppNotificationResult =
| { | {
state: "error"; state: "error";
error: unknown; error: unknown;
@ -14,7 +14,7 @@ type SendFrameNotificationResult =
| { state: "rate_limit" } | { state: "rate_limit" }
| { state: "success" }; | { state: "success" };
export async function sendFrameNotification({ export async function sendMiniAppNotification({
fid, fid,
title, title,
body, body,
@ -22,7 +22,7 @@ export async function sendFrameNotification({
fid: number; fid: number;
title: string; title: string;
body: string; body: string;
}): Promise<SendFrameNotificationResult> { }): Promise<SendMiniAppNotificationResult> {
const notificationDetails = await getUserNotificationDetails(fid); const notificationDetails = await getUserNotificationDetails(fid);
if (!notificationDetails) { if (!notificationDetails) {
return { state: "no_token" }; return { state: "no_token" };

View File

@ -4,7 +4,7 @@ import { mnemonicToAccount } from 'viem/accounts';
import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, APP_ICON_URL, APP_NAME, APP_OG_IMAGE_URL, APP_PRIMARY_CATEGORY, APP_SPLASH_BACKGROUND_COLOR, APP_TAGS, APP_URL, APP_WEBHOOK_URL } from './constants';
import { APP_SPLASH_URL } from './constants'; import { APP_SPLASH_URL } from './constants';
interface FrameMetadata { interface MiniAppMetadata {
version: string; version: string;
name: string; name: string;
iconUrl: string; iconUrl: string;
@ -19,13 +19,13 @@ interface FrameMetadata {
tags?: string[]; tags?: string[];
}; };
interface FrameManifest { interface MiniAppManifest {
accountAssociation?: { accountAssociation?: {
header: string; header: string;
payload: string; payload: string;
signature: string; signature: string;
}; };
frame: FrameMetadata; frame: MiniAppMetadata;
} }
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
@ -43,7 +43,7 @@ export function getSecretEnvVars() {
return { seedPhrase, fid }; return { seedPhrase, fid };
} }
export function getFrameEmbedMetadata(ogImageUrl?: string) { export function getMiniAppEmbedMetadata(ogImageUrl?: string) {
return { return {
version: "next", version: "next",
imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL, imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL,
@ -64,15 +64,15 @@ export function getFrameEmbedMetadata(ogImageUrl?: string) {
}; };
} }
export async function getFarcasterMetadata(): Promise<FrameManifest> { export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
// First check for FRAME_METADATA in .env and use that if it exists // First check for MINI_APP_METADATA in .env and use that if it exists
if (process.env.FRAME_METADATA) { if (process.env.MINI_APP_METADATA) {
try { try {
const metadata = JSON.parse(process.env.FRAME_METADATA); const metadata = JSON.parse(process.env.MINI_APP_METADATA);
console.log('Using pre-signed frame metadata from environment'); console.log('Using pre-signed mini app metadata from environment');
return metadata; return metadata;
} catch (error) { } catch (error) {
console.warn('Failed to parse FRAME_METADATA from environment:', error); console.warn('Failed to parse MINI_APP_METADATA from environment:', error);
} }
} }
@ -123,11 +123,11 @@ export async function getFarcasterMetadata(): Promise<FrameManifest> {
accountAssociation, accountAssociation,
frame: { frame: {
version: "1", version: "1",
name: APP_NAME ?? "Frames v2 Demo", name: APP_NAME ?? "Neynar Starter Kit",
iconUrl: APP_ICON_URL, iconUrl: APP_ICON_URL,
homeUrl: APP_URL, homeUrl: APP_URL,
imageUrl: APP_OG_IMAGE_URL, imageUrl: APP_OG_IMAGE_URL,
buttonTitle: APP_BUTTON_TEXT ?? "Launch Frame", buttonTitle: APP_BUTTON_TEXT ?? "Launch Mini App",
splashImageUrl: APP_SPLASH_URL, splashImageUrl: APP_SPLASH_URL,
splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR, splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR,
webhookUrl: APP_WEBHOOK_URL, webhookUrl: APP_WEBHOOK_URL,

View File

@ -1,7 +1,7 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
export default { export default {
darkMode: ["class"], darkMode: "media",
content: [ content: [
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}", "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
"./src/components/**/*.{js,ts,jsx,tsx,mdx}", "./src/components/**/*.{js,ts,jsx,tsx,mdx}",