mirror of
https://github.com/neynarxyz/create-farcaster-mini-app.git
synced 2025-11-16 08:08:56 -05:00
refactor: update frame to mini app and fix dark mode
This commit is contained in:
parent
cfe51f067c
commit
4831308983
@ -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.
|
||||||
|
|
||||||
|
|||||||
14
bin/init.js
14
bin/init.js
@ -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}"`);
|
||||||
|
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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()),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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`;
|
||||||
|
|||||||
@ -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];
|
||||||
|
|||||||
@ -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" };
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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}",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user