Format after fixing conflicts

This commit is contained in:
Shreyaschorge 2025-07-14 20:04:44 +05:30
parent 505aa54b16
commit e74b2581df
No known key found for this signature in database
30 changed files with 515 additions and 469 deletions

View File

@ -15,48 +15,48 @@ if (yIndex !== -1) {
args.splice(yIndex, 1); // Remove -y from args
}
// Parse other arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
// Parse other arguments
for (let i = 0; i < args.length; i++) {
const arg = args[i];
if (arg === '-p' || arg === '--project') {
if (i + 1 < args.length) {
projectName = args[i + 1];
if (projectName.startsWith('-')) {
console.error('Error: Project name cannot start with a dash (-)');
process.exit(1);
}
args.splice(i, 2); // Remove both the flag and its value
i--; // Adjust index since we removed 2 elements
} else {
console.error('Error: -p/--project requires a project name');
if (arg === '-p' || arg === '--project') {
if (i + 1 < args.length) {
projectName = args[i + 1];
if (projectName.startsWith('-')) {
console.error('Error: Project name cannot start with a dash (-)');
process.exit(1);
}
} else if (arg === '-k' || arg === '--api-key') {
if (i + 1 < args.length) {
apiKey = args[i + 1];
if (apiKey.startsWith('-')) {
console.error('Error: API key cannot start with a dash (-)');
process.exit(1);
}
args.splice(i, 2); // Remove both the flag and its value
i--; // Adjust index since we removed 2 elements
} else {
console.error('Error: -k/--api-key requires an API key');
args.splice(i, 2); // Remove both the flag and its value
i--; // Adjust index since we removed 2 elements
} else {
console.error('Error: -p/--project requires a project name');
process.exit(1);
}
} else if (arg === '-k' || arg === '--api-key') {
if (i + 1 < args.length) {
apiKey = args[i + 1];
if (apiKey.startsWith('-')) {
console.error('Error: API key cannot start with a dash (-)');
process.exit(1);
}
args.splice(i, 2); // Remove both the flag and its value
i--; // Adjust index since we removed 2 elements
} else {
console.error('Error: -k/--api-key requires an API key');
process.exit(1);
}
}
}
// Validate that if -y is used, a project name must be provided
if (autoAcceptDefaults && !projectName) {
console.error('Error: -y flag requires a project name. Use -p/--project to specify the project name.');
console.error(
'Error: -y flag requires a project name. Use -p/--project to specify the project name.',
);
process.exit(1);
}
init(projectName, autoAcceptDefaults, apiKey).catch((err) => {
init(projectName, autoAcceptDefaults, apiKey).catch(err => {
console.error('Error:', err);
process.exit(1);
});

View File

@ -1,19 +1,19 @@
#!/usr/bin/env node
import inquirer from 'inquirer';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
import fs from 'fs';
import { dirname } from 'path';
import path from 'path';
import { fileURLToPath } from 'url';
import inquirer from 'inquirer';
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git';
const SCRIPT_VERSION = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
).version;
// ANSI color codes
@ -47,12 +47,12 @@ async function queryNeynarApp(apiKey) {
}
try {
const response = await fetch(
`https://api.neynar.com/portal/app_by_api_key?starter_kit=true`,
'https://api.neynar.com/portal/app_by_api_key?starter_kit=true',
{
headers: {
'x-api-key': apiKey,
},
}
},
);
const data = await response.json();
return data;
@ -63,7 +63,11 @@ async function queryNeynarApp(apiKey) {
}
// Export the main CLI function for programmatic use
export async function init(projectName = null, autoAcceptDefaults = false, apiKey = null) {
export async function init(
projectName = null,
autoAcceptDefaults = false,
apiKey = null,
) {
printWelcomeMessage();
// Ask about Neynar usage
@ -107,7 +111,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
} else {
if (!autoAcceptDefaults) {
console.log(
'\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n'
'\n🪐 Find your Neynar API key at: https://dev.neynar.com/app\n',
);
}
@ -144,13 +148,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
if (useDemoKey.useDemo) {
console.warn(
'\n⚠ Note: the demo key is for development purposes only and is aggressively rate limited.'
'\n⚠ Note: the demo key is for development purposes only and is aggressively rate limited.',
);
console.log(
'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.'
'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.',
);
console.log(
`\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`
`\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`,
);
neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO';
}
@ -163,7 +167,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
break;
}
console.log(
'\n⚠ No valid API key provided. Would you like to try again?'
'\n⚠ No valid API key provided. Would you like to try again?',
);
const { retry } = await inquirer.prompt([
{
@ -239,7 +243,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
name: 'projectName',
message: 'What is the name of your mini app?',
default: projectName || defaultMiniAppName,
validate: (input) => {
validate: input => {
if (input.trim() === '') {
return 'Project name cannot be empty';
}
@ -286,13 +290,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
message:
'Enter tags for your mini app (separate with spaces or commas, optional):',
default: '',
filter: (input) => {
filter: input => {
if (!input.trim()) return [];
// Split by both spaces and commas, trim whitespace, and filter out empty strings
return input
.split(/[,\s]+/)
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0);
.map(tag => tag.trim())
.filter(tag => tag.length > 0);
},
},
{
@ -300,7 +304,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
name: 'buttonText',
message: 'Enter the button text for your mini app:',
default: 'Launch Mini App',
validate: (input) => {
validate: input => {
if (input.trim() === '') {
return 'Button text cannot be empty';
}
@ -370,8 +374,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
{
type: 'password',
name: 'seedPhrase',
message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
validate: (input) => {
message:
'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
validate: input => {
if (!input || input.trim().split(' ').length < 12) {
return 'Seed phrase must be at least 12 words';
}
@ -439,7 +444,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
// Update package.json
console.log('\nUpdating package.json...');
const packageJsonPath = path.join(projectPath, 'package.json');
let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
packageJson.name = finalProjectName;
packageJson.version = '0.1.0';
@ -522,21 +527,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
let constantsContent = fs.readFileSync(constantsPath, 'utf8');
// Helper function to escape single quotes in strings
const escapeString = (str) => str.replace(/'/g, "\\'");
const escapeString = str => str.replace(/'/g, "\\'");
// Helper function to safely replace constants with validation
const safeReplace = (content, pattern, replacement, constantName) => {
const match = content.match(pattern);
if (!match) {
console.log(
`⚠️ Warning: Could not update ${constantName} in constants.ts. Pattern not found.`
`⚠️ Warning: Could not update ${constantName} in constants.ts. Pattern not found.`,
);
console.log(`Pattern: ${pattern}`);
console.log(
`Expected to match in: ${
content.split('\n').find((line) => line.includes(constantName)) ||
content.split('\n').find(line => line.includes(constantName)) ||
'Not found'
}`
}`,
);
} else {
const newContent = content.replace(pattern, replacement);
@ -565,7 +570,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.APP_NAME,
`export const APP_NAME = '${escapeString(answers.projectName)}';`,
'APP_NAME'
'APP_NAME',
);
// Update APP_DESCRIPTION
@ -573,9 +578,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.APP_DESCRIPTION,
`export const APP_DESCRIPTION = '${escapeString(
answers.description
answers.description,
)}';`,
'APP_DESCRIPTION'
'APP_DESCRIPTION',
);
// Update APP_PRIMARY_CATEGORY (always update, null becomes empty string)
@ -583,21 +588,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.APP_PRIMARY_CATEGORY,
`export const APP_PRIMARY_CATEGORY = '${escapeString(
answers.primaryCategory || ''
answers.primaryCategory || '',
)}';`,
'APP_PRIMARY_CATEGORY'
'APP_PRIMARY_CATEGORY',
);
// Update APP_TAGS
const tagsString =
answers.tags.length > 0
? `['${answers.tags.map((tag) => escapeString(tag)).join("', '")}']`
? `['${answers.tags.map(tag => escapeString(tag)).join("', '")}']`
: "['neynar', 'starter-kit', 'demo']";
constantsContent = safeReplace(
constantsContent,
patterns.APP_TAGS,
`export const APP_TAGS = ${tagsString};`,
'APP_TAGS'
'APP_TAGS',
);
// Update APP_BUTTON_TEXT (always update, use answers value)
@ -605,9 +610,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.APP_BUTTON_TEXT,
`export const APP_BUTTON_TEXT = '${escapeString(
answers.buttonText || ''
answers.buttonText || '',
)}';`,
'APP_BUTTON_TEXT'
'APP_BUTTON_TEXT',
);
// Update USE_WALLET
@ -615,7 +620,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.USE_WALLET,
`export const USE_WALLET = ${answers.useWallet};`,
'USE_WALLET'
'USE_WALLET',
);
// Update ANALYTICS_ENABLED
@ -623,7 +628,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent,
patterns.ANALYTICS_ENABLED,
`export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`,
'ANALYTICS_ENABLED'
'ANALYTICS_ENABLED',
);
fs.writeFileSync(constantsPath, constantsContent);
@ -633,14 +638,14 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.appendFileSync(
envPath,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`,
);
if (useNeynar && neynarApiKey && neynarClientId) {
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
} else if (useNeynar) {
console.log(
'\n⚠ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID'
'\n⚠ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID',
);
}
if (answers.seedPhrase) {
@ -651,7 +656,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.unlinkSync(envExamplePath);
} else {
console.log(
'\n.env.example does not exist, skipping copy and remove operations'
'\n.env.example does not exist, skipping copy and remove operations',
);
}
@ -696,7 +701,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
execSync('git add .', { cwd: projectPath });
execSync(
'git commit -m "initial commit from @neynar/create-farcaster-mini-app"',
{ cwd: projectPath }
{ cwd: projectPath },
);
// Calculate border length based on message length

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "@neynar/create-farcaster-mini-app",
"version": "1.5.3",
"version": "1.5.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@neynar/create-farcaster-mini-app",
"version": "1.5.3",
"version": "1.5.9",
"dependencies": {
"dotenv": "^16.4.7",
"inquirer": "^12.4.3",

View File

@ -1,36 +1,37 @@
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import inquirer from "inquirer";
import dotenv from "dotenv";
import crypto from "crypto";
import { execSync } from 'child_process';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import dotenv from 'dotenv';
import inquirer from 'inquirer';
// Load environment variables in specific order
// First load .env for main config
dotenv.config({ path: ".env" });
dotenv.config({ path: '.env' });
async function loadEnvLocal() {
try {
if (fs.existsSync(".env.local")) {
if (fs.existsSync('.env.local')) {
const { loadLocal } = await inquirer.prompt([
{
type: "confirm",
name: "loadLocal",
type: 'confirm',
name: 'loadLocal',
message:
"Found .env.local, likely created by the install script - would you like to load its values?",
'Found .env.local, likely created by the install script - would you like to load its values?',
default: false,
},
]);
const localEnv = dotenv.parse(fs.readFileSync('.env.local'));
if (loadLocal) {
console.log("Loading values from .env.local...");
const localEnv = dotenv.parse(fs.readFileSync(".env.local"));
console.log('Loading values from .env.local...');
// Copy all values to .env
const envContent = fs.existsSync(".env")
? fs.readFileSync(".env", "utf8") + "\n"
: "";
const envContent = fs.existsSync('.env')
? fs.readFileSync('.env', 'utf8') + '\n'
: '';
let newEnvContent = envContent;
for (const [key, value] of Object.entries(localEnv)) {
@ -43,8 +44,8 @@ async function loadEnvLocal() {
}
// Write updated content to .env
fs.writeFileSync(".env", newEnvContent);
console.log("✅ Values from .env.local have been written to .env");
fs.writeFileSync('.env', newEnvContent);
console.log('✅ Values from .env.local have been written to .env');
}
if (localEnv.SPONSOR_SIGNER) {
process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER;
@ -52,26 +53,26 @@ async function loadEnvLocal() {
}
} catch (error) {
// Error reading .env.local, which is fine
console.log("Note: No .env.local file found");
console.log('Note: No .env.local file found');
}
}
// TODO: make sure rebuilding is supported
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, "..");
const projectRoot = path.join(__dirname, '..');
async function validateDomain(domain) {
// Remove http:// or https:// if present
const cleanDomain = domain.replace(/^https?:\/\//, "");
const cleanDomain = domain.replace(/^https?:\/\//, '');
// Basic domain validation
if (
!cleanDomain.match(
/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/
/^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/,
)
) {
throw new Error("Invalid domain format");
throw new Error('Invalid domain format');
}
return cleanDomain;
@ -83,39 +84,39 @@ async function queryNeynarApp(apiKey) {
}
try {
const response = await fetch(
`https://api.neynar.com/portal/app_by_api_key`,
'https://api.neynar.com/portal/app_by_api_key',
{
headers: {
"x-api-key": apiKey,
'x-api-key': apiKey,
},
}
},
);
const data = await response.json();
return data;
} catch (error) {
console.error("Error querying Neynar app data:", error);
console.error('Error querying Neynar app data:', error);
return null;
}
}
async function generateFarcasterMetadata(domain, webhookUrl) {
const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(",");
const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(',');
return {
accountAssociation: {
header: "",
payload: "",
signature: "",
header: '',
payload: '',
signature: '',
},
frame: {
version: "1",
version: '1',
name: process.env.NEXT_PUBLIC_MINI_APP_NAME,
iconUrl: `https://${domain}/icon.png`,
homeUrl: `https://${domain}`,
imageUrl: `https://${domain}/api/opengraph-image`,
buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT,
splashImageUrl: `https://${domain}/splash.png`,
splashBackgroundColor: "#f7f7f7",
splashBackgroundColor: '#f7f7f7',
webhookUrl,
description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION,
primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY,
@ -126,8 +127,8 @@ async function generateFarcasterMetadata(domain, webhookUrl) {
async function main() {
try {
console.log("\n📝 Checking environment variables...");
console.log("Loading values from .env...");
console.log('\n📝 Checking environment variables...');
console.log('Loading values from .env...');
// Load .env.local if user wants to
await loadEnvLocal();
@ -135,11 +136,11 @@ async function main() {
// Get domain from user
const { domain } = await inquirer.prompt([
{
type: "input",
name: "domain",
type: 'input',
name: 'domain',
message:
"Enter the domain where your mini app will be deployed (e.g., example.com):",
validate: async (input) => {
'Enter the domain where your mini app will be deployed (e.g., example.com):',
validate: async input => {
try {
await validateDomain(input);
return true;
@ -153,13 +154,13 @@ async function main() {
// Get frame name from user
const { frameName } = await inquirer.prompt([
{
type: "input",
name: "frameName",
message: "Enter the name for your mini app (e.g., My Cool Mini App):",
type: 'input',
name: 'frameName',
message: 'Enter the name for your mini app (e.g., My Cool Mini App):',
default: process.env.NEXT_PUBLIC_MINI_APP_NAME,
validate: (input) => {
if (input.trim() === "") {
return "Mini app name cannot be empty";
validate: input => {
if (input.trim() === '') {
return 'Mini app name cannot be empty';
}
return true;
},
@ -169,14 +170,14 @@ async function main() {
// Get button text from user
const { buttonText } = await inquirer.prompt([
{
type: "input",
name: "buttonText",
message: "Enter the text for your mini app button:",
type: 'input',
name: 'buttonText',
message: 'Enter the text for your mini app button:',
default:
process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || "Launch Mini App",
validate: (input) => {
if (input.trim() === "") {
return "Button text cannot be empty";
process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App',
validate: input => {
if (input.trim() === '') {
return 'Button text cannot be empty';
}
return true;
},
@ -192,16 +193,16 @@ async function main() {
if (!neynarApiKey) {
const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([
{
type: "password",
name: "neynarApiKey",
type: 'password',
name: 'neynarApiKey',
message:
"Enter your Neynar API key (optional - leave blank to skip):",
'Enter your Neynar API key (optional - leave blank to skip):',
default: null,
},
]);
neynarApiKey = inputNeynarApiKey;
} else {
console.log("Using existing Neynar API key from .env");
console.log('Using existing Neynar API key from .env');
}
if (!neynarApiKey) {
@ -214,7 +215,7 @@ async function main() {
const appInfo = await queryNeynarApp(neynarApiKey);
if (appInfo) {
neynarClientId = appInfo.app_uuid;
console.log("✅ Fetched Neynar app client ID");
console.log('✅ Fetched Neynar app client ID');
break;
}
}
@ -226,13 +227,13 @@ async function main() {
// If we get here, the API key was invalid
console.log(
"\n⚠ Could not find Neynar app information. The API key may be incorrect."
'\n⚠ Could not find Neynar app information. The API key may be incorrect.',
);
const { retry } = await inquirer.prompt([
{
type: "confirm",
name: "retry",
message: "Would you like to try a different API key?",
type: 'confirm',
name: 'retry',
message: 'Would you like to try a different API key?',
default: true,
},
]);
@ -248,7 +249,7 @@ async function main() {
}
// Generate manifest
console.log("\n🔨 Generating mini app manifest...");
console.log('\n🔨 Generating mini app manifest...');
// Determine webhook URL based on environment variables
const webhookUrl =
@ -257,13 +258,13 @@ async function main() {
: `https://${domain}/api/webhook`;
const metadata = await generateFarcasterMetadata(domain, webhookUrl);
console.log("\n✅ Mini app manifest generated");
console.log('\n✅ Mini app manifest generated');
// Read existing .env file or create new one
const envPath = path.join(projectRoot, ".env");
const envPath = path.join(projectRoot, '.env');
let envContent = fs.existsSync(envPath)
? fs.readFileSync(envPath, "utf8")
: "";
? fs.readFileSync(envPath, 'utf8')
: '';
// Add or update environment variables
const newEnvVars = [
@ -273,19 +274,19 @@ async function main() {
// Mini app metadata
`NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`,
`NEXT_PUBLIC_MINI_APP_DESCRIPTION="${
process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || ""
process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || ''
}"`,
`NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${
process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || ""
process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || ''
}"`,
`NEXT_PUBLIC_MINI_APP_TAGS="${
process.env.NEXT_PUBLIC_MINI_APP_TAGS || ""
process.env.NEXT_PUBLIC_MINI_APP_TAGS || ''
}"`,
`NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`,
// Analytics
`NEXT_PUBLIC_ANALYTICS_ENABLED="${
process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || "false"
process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false'
}"`,
// Neynar configuration (if it exists in current env)
@ -293,18 +294,19 @@ async function main() {
? [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`]
: []),
...(neynarClientId ? [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []),
...(process.env.SPONSOR_SIGNER ?
[`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] : []),
...(process.env.SPONSOR_SIGNER
? [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`]
: []),
// FID (if it exists in current env)
...(process.env.FID ? [`FID="${process.env.FID}"`] : []),
`NEXT_PUBLIC_USE_WALLET="${
process.env.NEXT_PUBLIC_USE_WALLET || "false"
process.env.NEXT_PUBLIC_USE_WALLET || 'false'
}"`,
// NextAuth configuration
`NEXTAUTH_SECRET="${
process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex")
process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex')
}"`,
`NEXTAUTH_URL="https://${domain}"`,
@ -313,14 +315,14 @@ async function main() {
];
// Filter out empty values and join with newlines
const validEnvVars = newEnvVars.filter((line) => {
const [, value] = line.split("=");
const validEnvVars = newEnvVars.filter(line => {
const [, value] = line.split('=');
return value && value !== '""';
});
// Update or append each environment variable
validEnvVars.forEach((varLine) => {
const [key] = varLine.split("=");
validEnvVars.forEach(varLine => {
const [key] = varLine.split('=');
if (envContent.includes(`${key}=`)) {
envContent = envContent.replace(new RegExp(`${key}=.*`), varLine);
} else {
@ -331,27 +333,27 @@ async function main() {
// Write updated .env file
fs.writeFileSync(envPath, envContent);
console.log("\n✅ Environment variables updated");
console.log('\n✅ Environment variables updated');
// Run next build
console.log("\nBuilding Next.js application...");
console.log('\nBuilding Next.js application...');
const nextBin = path.normalize(
path.join(projectRoot, "node_modules", ".bin", "next")
path.join(projectRoot, 'node_modules', '.bin', 'next'),
);
execSync(`"${nextBin}" build`, {
cwd: projectRoot,
stdio: "inherit",
shell: process.platform === "win32",
stdio: 'inherit',
shell: process.platform === 'win32',
});
console.log(
"\n✨ Build complete! Your mini app is ready for deployment. 🪐"
'\n✨ Build complete! Your mini app is ready for deployment. 🪐',
);
console.log(
"📝 Make sure to configure the environment variables from .env in your hosting provider"
'📝 Make sure to configure the environment variables from .env in your hosting provider',
);
} catch (error) {
console.error("\n❌ Error:", error.message);
console.error('\n❌ Error:', error.message);
process.exit(1);
}
}

View File

@ -1,12 +1,12 @@
import { execSync, spawn } from 'child_process';
import fs from 'fs';
import path from 'path';
import os from 'os';
import { fileURLToPath } from 'url';
import inquirer from 'inquirer';
import dotenv from 'dotenv';
import crypto from 'crypto';
import fs from 'fs';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import { Vercel } from '@vercel/sdk';
import dotenv from 'dotenv';
import inquirer from 'inquirer';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..');
@ -99,20 +99,19 @@ async function checkRequiredEnvVars() {
name: 'NEXT_PUBLIC_MINI_APP_NAME',
message: 'Enter the name for your frame (e.g., My Cool Mini App):',
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_MINI_APP_BUTTON_TEXT',
message: 'Enter the text for your frame button:',
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',
},
];
const missingVars = requiredVars.filter(
(varConfig) => !process.env[varConfig.name]
varConfig => !process.env[varConfig.name],
);
if (missingVars.length > 0) {
@ -138,7 +137,7 @@ async function checkRequiredEnvVars() {
const newLine = envContent ? '\n' : '';
fs.appendFileSync(
'.env',
`${newLine}${varConfig.name}="${value.trim()}"`
`${newLine}${varConfig.name}="${value.trim()}"`,
);
}
@ -161,7 +160,7 @@ async function checkRequiredEnvVars() {
if (storeSeedPhrase) {
fs.appendFileSync(
'.env.local',
`\nSPONSOR_SIGNER="${sponsorSigner}"`
`\nSPONSOR_SIGNER="${sponsorSigner}"`,
);
console.log('✅ Sponsor signer preference stored in .env.local');
}
@ -244,7 +243,7 @@ async function getVercelToken() {
return null; // We'll fall back to CLI operations
} catch (error) {
throw new Error(
'Not logged in to Vercel CLI. Please run this script again to login.'
'Not logged in to Vercel CLI. Please run this script again to login.',
);
}
}
@ -260,7 +259,7 @@ async function loginToVercel() {
console.log('3. Complete the Vercel account setup in your browser');
console.log('4. Return here once your Vercel account is created\n');
console.log(
'\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account'
'\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account',
);
const child = spawn('vercel', ['login'], {
@ -268,14 +267,14 @@ async function loginToVercel() {
});
await new Promise((resolve, reject) => {
child.on('close', (code) => {
child.on('close', code => {
resolve();
});
});
console.log('\n📱 Waiting for login to complete...');
console.log(
"If you're creating a new account, please complete the Vercel account setup in your browser first."
"If you're creating a new account, please complete the Vercel account setup in your browser first.",
);
for (let i = 0; i < 150; i++) {
@ -287,7 +286,7 @@ async function loginToVercel() {
if (error.message.includes('Account not found')) {
console.log(' Waiting for Vercel account setup to complete...');
}
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
@ -313,7 +312,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) {
});
const existingVar = existingVars.envs?.find(
(env) => env.key === key && env.target?.includes('production')
env => env.key === key && env.target?.includes('production'),
);
if (existingVar) {
@ -345,7 +344,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) {
} catch (error) {
console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`,
error.message
error.message,
);
return false;
}
@ -400,7 +399,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) {
}
console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`,
error.message
error.message,
);
return false;
}
@ -410,7 +409,7 @@ async function setEnvironmentVariables(
vercelClient,
projectId,
envVars,
projectRoot
projectRoot,
) {
console.log('\n📝 Setting up environment variables...');
@ -435,12 +434,12 @@ async function setEnvironmentVariables(
}
// Report results
const failed = results.filter((r) => !r.success);
const failed = results.filter(r => !r.success);
if (failed.length > 0) {
console.warn(`\n⚠️ Failed to set ${failed.length} environment variables:`);
failed.forEach((r) => console.warn(` - ${r.key}`));
failed.forEach(r => console.warn(` - ${r.key}`));
console.warn(
'\nYou may need to set these manually in the Vercel dashboard.'
'\nYou may need to set these manually in the Vercel dashboard.',
);
}
@ -450,7 +449,7 @@ async function setEnvironmentVariables(
async function waitForDeployment(
vercelClient,
projectId,
maxWaitTime = 300000
maxWaitTime = 300000,
) {
// 5 minutes
console.log('\n⏳ Waiting for deployment to complete...');
@ -477,14 +476,14 @@ async function waitForDeployment(
}
// Still building, wait and check again
await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds
} else {
console.log('⏳ No deployment found yet, waiting...');
await new Promise((resolve) => setTimeout(resolve, 5000));
await new Promise(resolve => setTimeout(resolve, 5000));
}
} catch (error) {
console.warn('⚠️ Could not check deployment status:', error.message);
await new Promise((resolve) => setTimeout(resolve, 5000));
await new Promise(resolve => setTimeout(resolve, 5000));
}
}
@ -507,18 +506,18 @@ async function deployToVercel(useGitHub = false) {
framework: 'nextjs',
},
null,
2
)
2,
),
);
}
// Set up Vercel project
console.log('\n📦 Setting up Vercel project...');
console.log(
'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n'
'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n',
);
console.log(
'\n⚠ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n'
'\n⚠ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n',
);
// Use spawn instead of execSync for better error handling
@ -530,7 +529,7 @@ async function deployToVercel(useGitHub = false) {
});
await new Promise((resolve, reject) => {
vercelSetup.on('close', (code) => {
vercelSetup.on('close', code => {
if (code === 0 || code === null) {
console.log('✅ Vercel project setup completed');
resolve();
@ -540,25 +539,25 @@ async function deployToVercel(useGitHub = false) {
}
});
vercelSetup.on('error', (error) => {
vercelSetup.on('error', error => {
console.log('⚠️ Vercel setup command completed (this is normal)');
resolve(); // Don't reject, as this is often expected
});
});
// Wait a moment for project files to be written
await new Promise((resolve) => setTimeout(resolve, 2000));
await new Promise(resolve => setTimeout(resolve, 2000));
// Load project info
let projectId;
try {
const projectJson = JSON.parse(
fs.readFileSync('.vercel/project.json', 'utf8')
fs.readFileSync('.vercel/project.json', 'utf8'),
);
projectId = projectJson.projectId;
} catch (error) {
throw new Error(
'Failed to load project info. Please ensure the Vercel project was created successfully.'
'Failed to load project info. Please ensure the Vercel project was created successfully.',
);
}
@ -574,7 +573,7 @@ async function deployToVercel(useGitHub = false) {
}
} catch (error) {
console.warn(
'⚠️ Could not initialize Vercel SDK, falling back to CLI operations'
'⚠️ Could not initialize Vercel SDK, falling back to CLI operations',
);
}
@ -593,7 +592,7 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Using project name for domain:', domain);
} catch (error) {
console.warn(
'⚠️ Could not get project details via SDK, using CLI fallback'
'⚠️ Could not get project details via SDK, using CLI fallback',
);
}
}
@ -606,7 +605,7 @@ async function deployToVercel(useGitHub = false) {
{
cwd: projectRoot,
encoding: 'utf8',
}
},
);
const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/);
@ -622,7 +621,7 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Using project name for domain:', domain);
} else {
console.warn(
'⚠️ Could not determine project name from inspection, using fallback'
'⚠️ Could not determine project name from inspection, using fallback',
);
// Use a fallback domain based on project ID
domain = `project-${projectId.slice(-8)}.vercel.app`;
@ -670,8 +669,8 @@ async function deployToVercel(useGitHub = false) {
...Object.fromEntries(
Object.entries(process.env).filter(([key]) =>
key.startsWith('NEXT_PUBLIC_')
)
key.startsWith('NEXT_PUBLIC_'),
),
),
};
@ -680,7 +679,7 @@ async function deployToVercel(useGitHub = false) {
vercelClient,
projectId,
vercelEnv,
projectRoot
projectRoot,
);
// Deploy the project
@ -704,7 +703,7 @@ async function deployToVercel(useGitHub = false) {
});
await new Promise((resolve, reject) => {
vercelDeploy.on('close', (code) => {
vercelDeploy.on('close', code => {
if (code === 0) {
console.log('✅ Vercel deployment command completed');
resolve();
@ -714,7 +713,7 @@ async function deployToVercel(useGitHub = false) {
}
});
vercelDeploy.on('error', (error) => {
vercelDeploy.on('error', error => {
console.error('❌ Vercel deployment error:', error.message);
reject(error);
});
@ -728,7 +727,7 @@ async function deployToVercel(useGitHub = false) {
} catch (error) {
console.warn(
'⚠️ Could not verify deployment completion:',
error.message
error.message,
);
console.log(' Proceeding with domain verification...');
}
@ -744,7 +743,7 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Verified actual domain:', actualDomain);
} catch (error) {
console.warn(
'⚠️ Could not verify domain via SDK, using assumed domain'
'⚠️ Could not verify domain via SDK, using assumed domain',
);
}
}
@ -769,7 +768,7 @@ async function deployToVercel(useGitHub = false) {
fid,
await validateSeedPhrase(process.env.SEED_PHRASE),
process.env.SEED_PHRASE,
webhookUrl
webhookUrl,
);
updatedEnv.MINI_APP_METADATA = updatedMetadata;
}
@ -778,7 +777,7 @@ async function deployToVercel(useGitHub = false) {
vercelClient,
projectId,
updatedEnv,
projectRoot
projectRoot,
);
console.log('\n📦 Redeploying with correct domain...');
@ -789,7 +788,7 @@ async function deployToVercel(useGitHub = false) {
});
await new Promise((resolve, reject) => {
vercelRedeploy.on('close', (code) => {
vercelRedeploy.on('close', code => {
if (code === 0) {
console.log('✅ Redeployment completed');
resolve();
@ -799,7 +798,7 @@ async function deployToVercel(useGitHub = false) {
}
});
vercelRedeploy.on('error', (error) => {
vercelRedeploy.on('error', error => {
console.error('❌ Redeployment error:', error.message);
reject(error);
});
@ -811,7 +810,7 @@ async function deployToVercel(useGitHub = false) {
console.log('\n✨ Deployment complete! Your mini app is now live at:');
console.log(`🌐 https://${domain}`);
console.log(
'\n📝 You can manage your project at https://vercel.com/dashboard'
'\n📝 You can manage your project at https://vercel.com/dashboard',
);
} catch (error) {
console.error('\n❌ Deployment failed:', error.message);
@ -823,7 +822,7 @@ async function main() {
try {
console.log('🚀 Vercel Mini App Deployment (SDK Edition)');
console.log(
'This script will deploy your mini app to Vercel using the Vercel SDK.'
'This script will deploy your mini app to Vercel using the Vercel SDK.',
);
console.log('\nThe script will:');
console.log('1. Check for required environment variables');

View File

@ -10,7 +10,7 @@ export async function GET() {
console.error('Error fetching nonce:', error);
return NextResponse.json(
{ error: 'Failed to fetch nonce' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -10,7 +10,7 @@ export async function GET(request: Request) {
if (!message || !signature) {
return NextResponse.json(
{ error: 'Message and signature are required' },
{ status: 400 }
{ status: 400 },
);
}
@ -37,7 +37,7 @@ export async function GET(request: Request) {
console.error('Error in session-signers API:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -10,7 +10,7 @@ export async function POST() {
console.error('Error fetching signer:', error);
return NextResponse.json(
{ error: 'Failed to fetch signer' },
{ status: 500 }
{ status: 500 },
);
}
}
@ -22,7 +22,7 @@ export async function GET(request: Request) {
if (!signerUuid) {
return NextResponse.json(
{ error: 'signerUuid is required' },
{ status: 400 }
{ status: 400 },
);
}
@ -36,7 +36,7 @@ export async function GET(request: Request) {
console.error('Error fetching signed key:', error);
return NextResponse.json(
{ error: 'Failed to fetch signed key' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -1,10 +1,10 @@
import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
import { mnemonicToAccount } from 'viem/accounts';
import {
SIGNED_KEY_REQUEST_TYPE,
SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
} from '~/lib/constants';
import { getNeynarClient } from '~/lib/neynar';
const postRequiredFields = ['signerUuid', 'publicKey'];
@ -16,7 +16,7 @@ export async function POST(request: Request) {
if (!body[field]) {
return NextResponse.json(
{ error: `${field} is required` },
{ status: 400 }
{ status: 400 },
);
}
}
@ -26,7 +26,7 @@ export async function POST(request: Request) {
if (redirectUrl && typeof redirectUrl !== 'string') {
return NextResponse.json(
{ error: 'redirectUrl must be a string' },
{ status: 400 }
{ status: 400 },
);
}
@ -38,7 +38,7 @@ export async function POST(request: Request) {
if (!seedPhrase) {
return NextResponse.json(
{ error: 'App configuration missing (SEED_PHRASE or FID)' },
{ status: 500 }
{ status: 500 },
);
}
@ -85,7 +85,7 @@ export async function POST(request: Request) {
console.error('Error registering signed key:', error);
return NextResponse.json(
{ error: 'Failed to register signed key' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -13,7 +13,7 @@ export async function GET(request: Request) {
{
error: `${param} parameter is required`,
},
{ status: 400 }
{ status: 400 },
);
}
}
@ -32,7 +32,7 @@ export async function GET(request: Request) {
console.error('Error fetching signers:', error);
return NextResponse.json(
{ error: 'Failed to fetch signers' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -9,7 +9,7 @@ export async function POST(request: Request) {
if (!session?.user?.fid) {
return NextResponse.json(
{ error: 'No authenticated session found' },
{ status: 401 }
{ status: 401 },
);
}
@ -19,7 +19,7 @@ export async function POST(request: Request) {
if (!signers || !user) {
return NextResponse.json(
{ error: 'Signers and user are required' },
{ status: 400 }
{ status: 400 },
);
}
@ -40,7 +40,7 @@ export async function POST(request: Request) {
console.error('Error preparing session update:', error);
return NextResponse.json(
{ error: 'Failed to prepare session update' },
{ status: 500 }
{ status: 500 },
);
}
}

View File

@ -1,9 +1,9 @@
import { notificationDetailsSchema } from "@farcaster/miniapp-sdk";
import { NextRequest } from "next/server";
import { z } from "zod";
import { setUserNotificationDetails } from "~/lib/kv";
import { sendMiniAppNotification } from "~/lib/notifs";
import { sendNeynarMiniAppNotification } from "~/lib/neynar";
import { NextRequest } from 'next/server';
import { notificationDetailsSchema } from '@farcaster/miniapp-sdk';
import { z } from 'zod';
import { setUserNotificationDetails } from '~/lib/kv';
import { sendNeynarMiniAppNotification } from '~/lib/neynar';
import { sendMiniAppNotification } from '~/lib/notifs';
const requestSchema = z.object({
fid: z.number(),
@ -13,7 +13,8 @@ const requestSchema = z.object({
export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to store notification details
// as they will be managed by Neynar's system
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const neynarEnabled =
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const requestJson = await request.json();
const requestBody = requestSchema.safeParse(requestJson);
@ -21,7 +22,7 @@ export async function POST(request: NextRequest) {
if (requestBody.success === false) {
return Response.json(
{ success: false, errors: requestBody.error.errors },
{ status: 400 }
{ status: 400 },
);
}
@ -29,27 +30,29 @@ export async function POST(request: NextRequest) {
if (!neynarEnabled) {
await setUserNotificationDetails(
Number(requestBody.data.fid),
requestBody.data.notificationDetails
requestBody.data.notificationDetails,
);
}
// Use appropriate notification function based on Neynar status
const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification;
const sendNotification = neynarEnabled
? sendNeynarMiniAppNotification
: sendMiniAppNotification;
const sendResult = await sendNotification({
fid: Number(requestBody.data.fid),
title: "Test notification",
body: "Sent at " + new Date().toISOString(),
title: 'Test notification',
body: 'Sent at ' + new Date().toISOString(),
});
if (sendResult.state === "error") {
if (sendResult.state === 'error') {
return Response.json(
{ success: false, error: sendResult.error },
{ status: 500 }
{ status: 500 },
);
} else if (sendResult.state === "rate_limit") {
} else if (sendResult.state === 'rate_limit') {
return Response.json(
{ success: false, error: "Rate limited" },
{ status: 429 }
{ success: false, error: 'Rate limited' },
{ status: 429 },
);
}

View File

@ -1,20 +1,21 @@
import { NextRequest } from 'next/server';
import {
ParseWebhookEvent,
parseWebhookEvent,
verifyAppKeyWithNeynar,
} from "@farcaster/miniapp-node";
import { NextRequest } from "next/server";
import { APP_NAME } from "~/lib/constants";
} from '@farcaster/miniapp-node';
import { APP_NAME } from '~/lib/constants';
import {
deleteUserNotificationDetails,
setUserNotificationDetails,
} from "~/lib/kv";
import { sendMiniAppNotification } from "~/lib/notifs";
} from '~/lib/kv';
import { sendMiniAppNotification } from '~/lib/notifs';
export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to handle webhooks here
// as they will be handled by Neynar's webhook endpoint
const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
const neynarEnabled =
process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID;
if (neynarEnabled) {
return Response.json({ success: true });
}
@ -28,24 +29,24 @@ export async function POST(request: NextRequest) {
const error = e as ParseWebhookEvent.ErrorType;
switch (error.name) {
case "VerifyJsonFarcasterSignature.InvalidDataError":
case "VerifyJsonFarcasterSignature.InvalidEventDataError":
case 'VerifyJsonFarcasterSignature.InvalidDataError':
case 'VerifyJsonFarcasterSignature.InvalidEventDataError':
// The request data is invalid
return Response.json(
{ success: false, error: error.message },
{ status: 400 }
{ status: 400 },
);
case "VerifyJsonFarcasterSignature.InvalidAppKeyError":
case 'VerifyJsonFarcasterSignature.InvalidAppKeyError':
// The app key is invalid
return Response.json(
{ success: false, error: error.message },
{ status: 401 }
{ status: 401 },
);
case "VerifyJsonFarcasterSignature.VerifyAppKeyError":
case 'VerifyJsonFarcasterSignature.VerifyAppKeyError':
// Internal error verifying the app key (caller may want to try again)
return Response.json(
{ success: false, error: error.message },
{ status: 500 }
{ status: 500 },
);
}
}
@ -56,33 +57,33 @@ export async function POST(request: NextRequest) {
// Only handle notifications if Neynar is not enabled
// When Neynar is enabled, notifications are handled through their webhook
switch (event.event) {
case "frame_added":
case 'frame_added':
if (event.notificationDetails) {
await setUserNotificationDetails(fid, event.notificationDetails);
await sendMiniAppNotification({
fid,
title: `Welcome to ${APP_NAME}`,
body: "Mini app is now added to your client",
body: 'Mini app is now added to your client',
});
} else {
await deleteUserNotificationDetails(fid);
}
break;
case "frame_removed":
case 'frame_removed':
await deleteUserNotificationDetails(fid);
break;
case "notifications_enabled":
case 'notifications_enabled':
await setUserNotificationDetails(fid, event.notificationDetails);
await sendMiniAppNotification({
fid,
title: `Welcome to ${APP_NAME}`,
body: "Notifications are now enabled",
body: 'Notifications are now enabled',
});
break;
case "notifications_disabled":
case 'notifications_disabled':
await deleteUserNotificationDetails(fid);
break;
}

View File

@ -1,18 +1,18 @@
'use client';
import dynamic from 'next/dynamic';
import { AuthKitProvider } from '@farcaster/auth-kit';
import { MiniAppProvider } from '@neynar/react';
import type { Session } from 'next-auth';
import { SessionProvider } from 'next-auth/react';
import { MiniAppProvider } from '@neynar/react';
import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider';
import { ANALYTICS_ENABLED } from '~/lib/constants';
import { AuthKitProvider } from '@farcaster/auth-kit';
const WagmiProvider = dynamic(
() => import('~/components/providers/WagmiProvider'),
{
ssr: false,
}
},
);
export function Providers({

View File

@ -1,6 +1,6 @@
import { createAppClient, viemConnector } from '@farcaster/auth-client';
import { AuthOptions, getServerSession } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { createAppClient, viemConnector } from '@farcaster/auth-client';
declare module 'next-auth' {
interface Session {
@ -401,7 +401,7 @@ export const authOptions: AuthOptions = {
},
cookies: {
sessionToken: {
name: `next-auth.session-token`,
name: 'next-auth.session-token',
options: {
httpOnly: true,
sameSite: 'none',
@ -410,7 +410,7 @@ export const authOptions: AuthOptions = {
},
},
callbackUrl: {
name: `next-auth.callback-url`,
name: 'next-auth.callback-url',
options: {
sameSite: 'none',
path: '/',
@ -418,7 +418,7 @@ export const authOptions: AuthOptions = {
},
},
csrfToken: {
name: `next-auth.csrf-token`,
name: 'next-auth.csrf-token',
options: {
httpOnly: true,
sameSite: 'none',

View File

@ -1,10 +1,13 @@
import React, { createContext, useEffect, useState } from "react";
import dynamic from "next/dynamic";
import React, { createContext, useEffect, useState } from 'react';
import dynamic from 'next/dynamic';
import { sdk } from '@farcaster/miniapp-sdk';
const FarcasterSolanaProvider = dynamic(
() => import('@farcaster/mini-app-solana').then(mod => mod.FarcasterSolanaProvider),
{ ssr: false }
() =>
import('@farcaster/mini-app-solana').then(
mod => mod.FarcasterSolanaProvider,
),
{ ssr: false },
);
type SafeFarcasterSolanaProviderProps = {
@ -12,10 +15,15 @@ type SafeFarcasterSolanaProviderProps = {
children: React.ReactNode;
};
const SolanaProviderContext = createContext<{ hasSolanaProvider: boolean }>({ hasSolanaProvider: false });
const SolanaProviderContext = createContext<{ hasSolanaProvider: boolean }>({
hasSolanaProvider: false,
});
export function SafeFarcasterSolanaProvider({ endpoint, children }: SafeFarcasterSolanaProviderProps) {
const isClient = typeof window !== "undefined";
export function SafeFarcasterSolanaProvider({
endpoint,
children,
}: SafeFarcasterSolanaProviderProps) {
const isClient = typeof window !== 'undefined';
const [hasSolanaProvider, setHasSolanaProvider] = useState<boolean>(false);
const [checked, setChecked] = useState<boolean>(false);
@ -48,8 +56,8 @@ export function SafeFarcasterSolanaProvider({ endpoint, children }: SafeFarcaste
const origError = console.error;
console.error = (...args) => {
if (
typeof args[0] === "string" &&
args[0].includes("WalletConnectionError: could not get Solana provider")
typeof args[0] === 'string' &&
args[0].includes('WalletConnectionError: could not get Solana provider')
) {
if (!errorShown) {
origError(...args);

View File

@ -1,12 +1,12 @@
import { createConfig, http, WagmiProvider } from "wagmi";
import { base, degen, mainnet, optimism, unichain, celo } from "wagmi/chains";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { farcasterFrame } from "@farcaster/miniapp-wagmi-connector";
import React from 'react';
import { useEffect, useState } from 'react';
import { farcasterFrame } from '@farcaster/miniapp-wagmi-connector';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { createConfig, http, WagmiProvider } from 'wagmi';
import { useConnect, useAccount } from 'wagmi';
import { base, degen, mainnet, optimism, unichain, celo } from 'wagmi/chains';
import { coinbaseWallet, metaMask } from 'wagmi/connectors';
import { APP_NAME, APP_ICON_URL, APP_URL } from "~/lib/constants";
import { useEffect, useState } from "react";
import { useConnect, useAccount } from "wagmi";
import React from "react";
import { APP_NAME, APP_ICON_URL, APP_URL } from '~/lib/constants';
// Custom hook for Coinbase Wallet detection and auto-connection
function useCoinbaseWalletAutoConnect() {
@ -17,7 +17,8 @@ function useCoinbaseWalletAutoConnect() {
useEffect(() => {
// Check if we're running in Coinbase Wallet
const checkCoinbaseWallet = () => {
const isInCoinbaseWallet = window.ethereum?.isCoinbaseWallet ||
const isInCoinbaseWallet =
window.ethereum?.isCoinbaseWallet ||
window.ethereum?.isCoinbaseWalletExtension ||
window.ethereum?.isCoinbaseWalletBrowser;
setIsCoinbaseWallet(!!isInCoinbaseWallet);
@ -70,7 +71,11 @@ export const config = createConfig({
const queryClient = new QueryClient();
// Wrapper component that provides Coinbase Wallet auto-connection
function CoinbaseWalletAutoConnect({ children }: { children: React.ReactNode }) {
function CoinbaseWalletAutoConnect({
children,
}: {
children: React.ReactNode;
}) {
useCoinbaseWalletAutoConnect();
return <>{children}</>;
}
@ -79,9 +84,7 @@ export default function Provider({ children }: { children: React.ReactNode }) {
return (
<WagmiProvider config={config}>
<QueryClientProvider client={queryClient}>
<CoinbaseWalletAutoConnect>
{children}
</CoinbaseWalletAutoConnect>
<CoinbaseWalletAutoConnect>{children}</CoinbaseWalletAutoConnect>
</QueryClientProvider>
</WagmiProvider>
);

View File

@ -1,9 +1,9 @@
"use client";
'use client';
import { useState } from "react";
import { APP_NAME } from "~/lib/constants";
import sdk from "@farcaster/miniapp-sdk";
import { useMiniApp } from "@neynar/react";
import { useState } from 'react';
import sdk from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react';
import { APP_NAME } from '~/lib/constants';
type HeaderProps = {
neynarUser?: {
@ -18,12 +18,8 @@ export function Header({ neynarUser }: HeaderProps) {
return (
<div className="relative">
<div
className="mt-4 mb-4 mx-4 px-2 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-between border-[3px] border-double border-primary"
>
<div className="text-lg font-light">
Welcome to {APP_NAME}!
</div>
<div className="mt-4 mb-4 mx-4 px-2 py-2 bg-gray-100 dark:bg-gray-800 rounded-lg flex items-center justify-between border-[3px] border-double border-primary">
<div className="text-lg font-light">Welcome to {APP_NAME}!</div>
{context?.user && (
<div
className="cursor-pointer"
@ -49,7 +45,9 @@ export function Header({ neynarUser }: HeaderProps) {
<div className="text-right">
<h3
className="font-bold text-sm hover:underline cursor-pointer inline-block"
onClick={() => sdk.actions.viewProfile({ fid: context.user.fid })}
onClick={() =>
sdk.actions.viewProfile({ fid: context.user.fid })
}
>
{context.user.displayName || context.user.username}
</h3>

View File

@ -169,7 +169,7 @@ export function AuthDialog({
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(
content.qrUrl
content.qrUrl,
)}`}
alt="QR Code"
className="w-48 h-48"
@ -197,14 +197,14 @@ export function AuthDialog({
content.qrUrl
.replace(
'https://farcaster.xyz/',
'https://client.farcaster.xyz/deeplinks/'
'https://client.farcaster.xyz/deeplinks/',
)
.replace(
'https://client.farcaster.xyz/deeplinks/signed-key-request',
'https://farcaster.xyz/~/connect'
'https://farcaster.xyz/~/connect',
),
'_blank'
)
'_blank',
);
}
}}
className="btn btn-outline flex items-center justify-center gap-2 w-full"

View File

@ -27,7 +27,7 @@ export function ProfileButton({
'flex items-center gap-3 px-4 py-2 min-w-0 rounded-lg',
'bg-transparent border border-gray-300 dark:border-gray-600 text-gray-900 dark:text-gray-100',
'hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors',
'focus:outline-none focus:ring-1 focus:ring-primary'
'focus:outline-none focus:ring-1 focus:ring-primary',
)}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
@ -35,7 +35,7 @@ export function ProfileButton({
src={pfpUrl}
alt="Profile"
className="w-6 h-6 rounded-full object-cover flex-shrink-0"
onError={(e) => {
onError={e => {
(e.target as HTMLImageElement).src =
'https://farcaster.xyz/avatar.png';
}}
@ -46,7 +46,7 @@ export function ProfileButton({
<svg
className={cn(
'w-4 h-4 transition-transform flex-shrink-0',
showDropdown && 'rotate-180'
showDropdown && 'rotate-180',
)}
fill="none"
stroke="currentColor"

View File

@ -1,20 +1,20 @@
'use client';
import '@farcaster/auth-kit/styles.css';
import { useSignIn, UseSignInData } from '@farcaster/auth-kit';
import { useCallback, useEffect, useState, useRef } from 'react';
import { cn } from '~/lib/utils';
import { Button } from '~/components/ui/Button';
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
import { getItem, removeItem, setItem } from '~/lib/localStorage';
import { useSignIn, UseSignInData } from '@farcaster/auth-kit';
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk';
import { useMiniApp } from '@neynar/react';
import {
signIn as backendSignIn,
signOut as backendSignOut,
useSession,
} from 'next-auth/react';
import sdk, { SignIn as SignInCore } from '@farcaster/frame-sdk';
import { Button } from '~/components/ui/Button';
import { AuthDialog } from '~/components/ui/NeynarAuthButton/AuthDialog';
import { ProfileButton } from '~/components/ui/NeynarAuthButton/ProfileButton';
import { getItem, removeItem, setItem } from '~/lib/localStorage';
import { cn } from '~/lib/utils';
type User = {
fid: number;
@ -102,13 +102,13 @@ export function NeynarAuthButton() {
// New state for unified dialog flow
const [showDialog, setShowDialog] = useState(false);
const [dialogStep, setDialogStep] = useState<'signin' | 'access' | 'loading'>(
'loading'
'loading',
);
const [signerApprovalUrl, setSignerApprovalUrl] = useState<string | null>(
null
null,
);
const [pollingInterval, setPollingInterval] = useState<NodeJS.Timeout | null>(
null
null,
);
const [message, setMessage] = useState<string | null>(null);
const [signature, setSignature] = useState<string | null>(null);
@ -141,7 +141,7 @@ export function NeynarAuthButton() {
const updateSessionWithSigners = useCallback(
async (
signers: StoredAuthState['signers'],
user: StoredAuthState['user']
user: StoredAuthState['user'],
) => {
if (!useBackendFlow) return;
@ -164,7 +164,7 @@ export function NeynarAuthButton() {
console.error('❌ Error updating session with signers:', error);
}
},
[useBackendFlow, message, signature, nonce]
[useBackendFlow, message, signature, nonce],
);
// Helper function to fetch user data from Neynar API
@ -182,7 +182,7 @@ export function NeynarAuthButton() {
return null;
}
},
[]
[],
);
// Helper function to generate signed key request
@ -210,7 +210,7 @@ export function NeynarAuthButton() {
if (!response.ok) {
const errorData = await response.json();
throw new Error(
`Failed to generate signed key request: ${errorData.error}`
`Failed to generate signed key request: ${errorData.error}`,
);
}
@ -222,7 +222,7 @@ export function NeynarAuthButton() {
// throw error;
}
},
[]
[],
);
// Helper function to fetch all signers
@ -233,10 +233,10 @@ export function NeynarAuthButton() {
const endpoint = useBackendFlow
? `/api/auth/session-signers?message=${encodeURIComponent(
message
message,
)}&signature=${signature}`
: `/api/auth/signers?message=${encodeURIComponent(
message
message,
)}&signature=${signature}`;
const response = await fetch(endpoint);
@ -258,7 +258,7 @@ export function NeynarAuthButton() {
if (signerData.signers && signerData.signers.length > 0) {
const fetchedUser = (await fetchUserData(
signerData.signers[0].fid
signerData.signers[0].fid,
)) as StoredAuthState['user'];
user = fetchedUser;
}
@ -285,7 +285,7 @@ export function NeynarAuthButton() {
setSignersLoading(false);
}
},
[useBackendFlow, fetchUserData, updateSessionWithSigners]
[useBackendFlow, fetchUserData, updateSessionWithSigners],
);
// Helper function to poll signer status
@ -311,7 +311,7 @@ export function NeynarAuthButton() {
try {
const response = await fetch(
`/api/auth/signer?signerUuid=${signerUuid}`
`/api/auth/signer?signerUuid=${signerUuid}`,
);
if (!response.ok) {
@ -352,7 +352,7 @@ export function NeynarAuthButton() {
setPollingInterval(interval);
},
[fetchAllSigners, pollingInterval]
[fetchAllSigners, pollingInterval],
);
// Cleanup polling on unmount
@ -412,7 +412,7 @@ export function NeynarAuthButton() {
}
// For backend flow, the session will be handled by NextAuth
},
[useBackendFlow, fetchUserData]
[useBackendFlow, fetchUserData],
);
// Error callback
@ -459,7 +459,12 @@ export function NeynarAuthButton() {
// Handle fetching signers after successful authentication
useEffect(() => {
if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) {
if (
message &&
signature &&
!isSignerFlowRunning &&
!signerFlowStartedRef.current
) {
signerFlowStartedRef.current = true;
const handleSignerFlow = async () => {
@ -490,7 +495,7 @@ export function NeynarAuthButton() {
// Step 2: Generate signed key request
const signedKeyData = await generateSignedKeyRequest(
newSigner.signer_uuid,
newSigner.public_key
newSigner.public_key,
);
// Step 3: Show QR code in access dialog for signer approval
@ -501,8 +506,8 @@ export function NeynarAuthButton() {
await sdk.actions.openUrl(
signedKeyData.signer_approval_url.replace(
'https://client.farcaster.xyz/deeplinks/signed-key-request',
'https://farcaster.xyz/~/connect'
)
'https://farcaster.xyz/~/connect',
),
);
} else {
setShowDialog(true); // Ensure dialog is shown during loading
@ -663,7 +668,7 @@ export function NeynarAuthButton() {
'btn btn-primary flex items-center gap-3',
'disabled:opacity-50 disabled:cursor-not-allowed',
'transform transition-all duration-200 active:scale-[0.98]',
!url && !useBackendFlow && 'cursor-not-allowed'
!url && !useBackendFlow && 'cursor-not-allowed',
)}
>
{!useBackendFlow && !url ? (

View File

@ -1,9 +1,9 @@
'use client';
import { useCallback, useState, useEffect } from 'react';
import { Button } from './Button';
import { type ComposeCast } from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react';
import { type ComposeCast } from "@farcaster/miniapp-sdk";
import { Button } from './Button';
interface EmbedConfig {
path?: string;
@ -23,9 +23,16 @@ interface ShareButtonProps {
isLoading?: boolean;
}
export function ShareButton({ buttonText, cast, className = '', isLoading = false }: ShareButtonProps) {
export function ShareButton({
buttonText,
cast,
className = '',
isLoading = false,
}: ShareButtonProps) {
const [isProcessing, setIsProcessing] = useState(false);
const [bestFriends, setBestFriends] = useState<{ fid: number; username: string; }[] | null>(null);
const [bestFriends, setBestFriends] = useState<
{ fid: number; username: string }[] | null
>(null);
const [isLoadingBestFriends, setIsLoadingBestFriends] = useState(false);
const { context, actions } = useMiniApp();
@ -51,7 +58,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
if (cast.bestFriends) {
if (bestFriends) {
// Replace @N with usernames, or remove if no matching friend
finalText = finalText.replace(/@\d+/g, (match) => {
finalText = finalText.replace(/@\d+/g, match => {
const friendIndex = parseInt(match.slice(1)) - 1;
const friend = bestFriends[friendIndex];
if (friend) {
@ -67,16 +74,20 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
// Process embeds
const processedEmbeds = await Promise.all(
(cast.embeds || []).map(async (embed) => {
(cast.embeds || []).map(async embed => {
if (typeof embed === 'string') {
return embed;
}
if (embed.path) {
const baseUrl = process.env.NEXT_PUBLIC_URL || window.location.origin;
const baseUrl =
process.env.NEXT_PUBLIC_URL || window.location.origin;
const url = new URL(`${baseUrl}${embed.path}`);
// Add UTM parameters
url.searchParams.set('utm_source', `share-cast-${context?.user?.fid || 'unknown'}`);
url.searchParams.set(
'utm_source',
`share-cast-${context?.user?.fid || 'unknown'}`,
);
// If custom image generator is provided, use it
if (embed.imageUrl) {
@ -87,7 +98,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
return url.toString();
}
return embed.url || '';
})
}),
);
// Open cast composer with all supported intents

View File

@ -1,12 +1,12 @@
'use client';
import { useCallback, useState } from "react";
import { useMiniApp } from "@neynar/react";
import { ShareButton } from "../Share";
import { Button } from "../Button";
import { SignIn } from "../wallet/SignIn";
import { type Haptics } from "@farcaster/miniapp-sdk";
import { useCallback, useState } from 'react';
import { type Haptics } from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react';
import { Button } from '../Button';
import { NeynarAuthButton } from '../NeynarAuthButton/index';
import { ShareButton } from '../Share';
import { SignIn } from '../wallet/SignIn';
/**
* ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback.
@ -51,7 +51,7 @@ export function ActionsTab() {
* @returns Promise that resolves when the notification is sent or fails
*/
const sendFarcasterNotification = useCallback(async () => {
setNotificationState((prev) => ({ ...prev, sendStatus: '' }));
setNotificationState(prev => ({ ...prev, sendStatus: '' }));
if (!notificationDetails || !context) {
return;
}
@ -66,22 +66,22 @@ export function ActionsTab() {
}),
});
if (response.status === 200) {
setNotificationState((prev) => ({ ...prev, sendStatus: 'Success' }));
setNotificationState(prev => ({ ...prev, sendStatus: 'Success' }));
return;
} else if (response.status === 429) {
setNotificationState((prev) => ({
setNotificationState(prev => ({
...prev,
sendStatus: 'Rate limited',
}));
return;
}
const responseText = await response.text();
setNotificationState((prev) => ({
setNotificationState(prev => ({
...prev,
sendStatus: `Error: ${responseText}`,
}));
} catch (error) {
setNotificationState((prev) => ({
setNotificationState(prev => ({
...prev,
sendStatus: `Error: ${error}`,
}));
@ -98,11 +98,11 @@ export function ActionsTab() {
if (context?.user?.fid) {
const userShareUrl = `${process.env.NEXT_PUBLIC_URL}/share/${context.user.fid}`;
await navigator.clipboard.writeText(userShareUrl);
setNotificationState((prev) => ({ ...prev, shareUrlCopied: true }));
setNotificationState(prev => ({ ...prev, shareUrlCopied: true }));
setTimeout(
() =>
setNotificationState((prev) => ({ ...prev, shareUrlCopied: false })),
2000
setNotificationState(prev => ({ ...prev, shareUrlCopied: false })),
2000,
);
}
}, [context?.user?.fid]);
@ -123,10 +123,10 @@ export function ActionsTab() {
// --- Render ---
return (
<div className='space-y-3 px-6 w-full max-w-md mx-auto'>
<div className="space-y-3 px-6 w-full max-w-md mx-auto">
{/* Share functionality */}
<ShareButton
buttonText='Share Mini App'
buttonText="Share Mini App"
cast={{
text: 'Check out this awesome frame @1 @2 @3! 🚀🪐',
bestFriends: true,
@ -134,7 +134,7 @@ export function ActionsTab() {
`${process.env.NEXT_PUBLIC_URL}/share/${context?.user?.fid || ''}`,
],
}}
className='w-full'
className="w-full"
/>
{/* Authentication */}
@ -148,25 +148,25 @@ export function ActionsTab() {
onClick={() =>
actions.openUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
}
className='w-full'
className="w-full"
>
Open Link
</Button>
<Button onClick={actions.addMiniApp} disabled={added} className='w-full'>
<Button onClick={actions.addMiniApp} disabled={added} className="w-full">
Add Mini App to Client
</Button>
{/* Notification functionality */}
{notificationState.sendStatus && (
<div className='text-sm w-full'>
<div className="text-sm w-full">
Send notification result: {notificationState.sendStatus}
</div>
)}
<Button
onClick={sendFarcasterNotification}
disabled={!notificationDetails}
className='w-full'
className="w-full"
>
Send notification
</Button>
@ -175,24 +175,24 @@ export function ActionsTab() {
<Button
onClick={copyUserShareUrl}
disabled={!context?.user?.fid}
className='w-full'
className="w-full"
>
{notificationState.shareUrlCopied ? 'Copied!' : 'Copy share URL'}
</Button>
{/* Haptic feedback controls */}
<div className='space-y-2'>
<label className='block text-sm font-medium text-gray-700 dark:text-gray-300'>
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">
Haptic Intensity
</label>
<select
value={selectedHapticIntensity}
onChange={(e) =>
onChange={e =>
setSelectedHapticIntensity(
e.target.value as Haptics.ImpactOccurredType
e.target.value as Haptics.ImpactOccurredType,
)
}
className='w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary'
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value={'light'}>Light</option>
<option value={'medium'}>Medium</option>
@ -200,7 +200,7 @@ export function ActionsTab() {
<option value={'soft'}>Soft</option>
<option value={'rigid'}>Rigid</option>
</select>
<Button onClick={triggerHapticFeedback} className='w-full'>
<Button onClick={triggerHapticFeedback} className="w-full">
Trigger Haptic Feedback
</Button>
</div>

View File

@ -17,7 +17,9 @@ export function HomeTab() {
<div className="flex items-center justify-center h-[calc(100vh-200px)] px-6">
<div className="text-center w-full max-w-md mx-auto">
<p className="text-lg mb-2">Put your content here!</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Powered by Neynar 🪐</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
Powered by Neynar 🪐
</p>
</div>
</div>
);

View File

@ -1,10 +1,10 @@
'use client';
import { useCallback, useState } from "react";
import { signIn, signOut, getCsrfToken } from "next-auth/react";
import sdk, { SignIn as SignInCore } from "@farcaster/miniapp-sdk";
import { useSession } from "next-auth/react";
import { Button } from "../Button";
import { useCallback, useState } from 'react';
import sdk, { SignIn as SignInCore } from '@farcaster/miniapp-sdk';
import { signIn, signOut, getCsrfToken } from 'next-auth/react';
import { useSession } from 'next-auth/react';
import { Button } from '../Button';
/**
* SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF).
@ -72,7 +72,7 @@ export function SignIn() {
*/
const handleSignIn = useCallback(async () => {
try {
setAuthState((prev) => ({ ...prev, signingIn: true }));
setAuthState(prev => ({ ...prev, signingIn: true }));
setSignInFailure(undefined);
const nonce = await getNonce();
const result = await sdk.actions.signIn({ nonce });
@ -89,7 +89,7 @@ export function SignIn() {
}
setSignInFailure('Unknown error');
} finally {
setAuthState((prev) => ({ ...prev, signingIn: false }));
setAuthState(prev => ({ ...prev, signingIn: false }));
}
}, [getNonce]);
@ -103,14 +103,14 @@ export function SignIn() {
*/
const handleSignOut = useCallback(async () => {
try {
setAuthState((prev) => ({ ...prev, signingOut: true }));
setAuthState(prev => ({ ...prev, signingOut: true }));
// Only sign out if the current session is from Farcaster provider
if (session?.provider === 'farcaster') {
await signOut({ redirect: false });
}
setSignInResult(undefined);
} finally {
setAuthState((prev) => ({ ...prev, signingOut: false }));
setAuthState(prev => ({ ...prev, signingOut: false }));
}
}, [session]);
@ -132,7 +132,9 @@ export function SignIn() {
{/* Session Information */}
{session && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">Session</div>
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
Session
</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200">
{JSON.stringify(session, null, 2)}
</div>
@ -142,15 +144,21 @@ export function SignIn() {
{/* Error Display */}
{signInFailure && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200">{signInFailure}</div>
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
SIWF Result
</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200">
{signInFailure}
</div>
</div>
)}
{/* Success Result Display */}
{signInResult && !authState.signingIn && (
<div className="my-2 p-2 text-xs overflow-x-scroll bg-gray-100 dark:bg-gray-900 rounded-lg font-mono">
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">SIWF Result</div>
<div className="font-semibold text-gray-500 dark:text-gray-300 mb-1">
SIWF Result
</div>
<div className="whitespace-pre text-gray-700 dark:text-gray-200">
{JSON.stringify(signInResult, null, 2)}
</div>

View File

@ -2,7 +2,7 @@ import { useEffect } from 'react';
export function useDetectClickOutside<T extends HTMLElement>(
ref: React.RefObject<T | null>,
callback: () => void
callback: () => void,
) {
useEffect(() => {
function handleClickOutside(event: MouseEvent) {

View File

@ -1,23 +1,25 @@
import { FrameNotificationDetails } from "@farcaster/miniapp-sdk";
import { Redis } from "@upstash/redis";
import { APP_NAME } from "./constants";
import { FrameNotificationDetails } from '@farcaster/miniapp-sdk';
import { Redis } from '@upstash/redis';
import { APP_NAME } from './constants';
// In-memory fallback storage
const localStore = new Map<string, FrameNotificationDetails>();
// Use Redis if KV env vars are present, otherwise use in-memory
const useRedis = process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN;
const redis = useRedis ? new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
}) : null;
const redis = useRedis
? new Redis({
url: process.env.KV_REST_API_URL!,
token: process.env.KV_REST_API_TOKEN!,
})
: null;
function getUserNotificationDetailsKey(fid: number): string {
return `${APP_NAME}:user:${fid}`;
}
export async function getUserNotificationDetails(
fid: number
fid: number,
): Promise<FrameNotificationDetails | null> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {
@ -28,7 +30,7 @@ export async function getUserNotificationDetails(
export async function setUserNotificationDetails(
fid: number,
notificationDetails: FrameNotificationDetails
notificationDetails: FrameNotificationDetails,
): Promise<void> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {
@ -39,7 +41,7 @@ export async function setUserNotificationDetails(
}
export async function deleteUserNotificationDetails(
fid: number
fid: number,
): Promise<void> {
const key = getUserNotificationDetailsKey(fid);
if (redis) {

View File

@ -1,18 +1,18 @@
import {
SendNotificationRequest,
sendNotificationResponseSchema,
} from "@farcaster/miniapp-sdk";
import { getUserNotificationDetails } from "~/lib/kv";
import { APP_URL } from "./constants";
} from '@farcaster/miniapp-sdk';
import { getUserNotificationDetails } from '~/lib/kv';
import { APP_URL } from './constants';
type SendMiniAppNotificationResult =
| {
state: "error";
state: 'error';
error: unknown;
}
| { state: "no_token" }
| { state: "rate_limit" }
| { state: "success" };
| { state: 'no_token' }
| { state: 'rate_limit' }
| { state: 'success' };
export async function sendMiniAppNotification({
fid,
@ -25,13 +25,13 @@ export async function sendMiniAppNotification({
}): Promise<SendMiniAppNotificationResult> {
const notificationDetails = await getUserNotificationDetails(fid);
if (!notificationDetails) {
return { state: "no_token" };
return { state: 'no_token' };
}
const response = await fetch(notificationDetails.url, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({
notificationId: crypto.randomUUID(),
@ -48,17 +48,17 @@ export async function sendMiniAppNotification({
const responseBody = sendNotificationResponseSchema.safeParse(responseJson);
if (responseBody.success === false) {
// Malformed response
return { state: "error", error: responseBody.error.errors };
return { state: 'error', error: responseBody.error.errors };
}
if (responseBody.data.result.rateLimitedTokens.length) {
// Rate limited
return { state: "rate_limit" };
return { state: 'rate_limit' };
}
return { state: "success" };
return { state: 'success' };
} else {
// Error response
return { state: "error", error: responseJson };
return { state: 'error', error: responseJson };
}
}

View File

@ -1,6 +1,5 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
import { mnemonicToAccount } from "viem/accounts";
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
import {
APP_BUTTON_TEXT,
APP_DESCRIPTION,
@ -12,8 +11,8 @@ import {
APP_TAGS,
APP_URL,
APP_WEBHOOK_URL,
} from "./constants";
import { APP_SPLASH_URL } from "./constants";
} from './constants';
import { APP_SPLASH_URL } from './constants';
interface MiniAppMetadata {
version: string;
@ -45,12 +44,12 @@ export function cn(...inputs: ClassValue[]) {
export function getMiniAppEmbedMetadata(ogImageUrl?: string) {
return {
version: "next",
version: 'next',
imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL,
button: {
title: APP_BUTTON_TEXT,
action: {
type: "launch_frame",
type: 'launch_frame',
name: APP_NAME,
url: APP_URL,
splashImageUrl: APP_SPLASH_URL,
@ -69,37 +68,37 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
if (process.env.MINI_APP_METADATA) {
try {
const metadata = JSON.parse(process.env.MINI_APP_METADATA);
console.log("Using pre-signed mini app metadata from environment");
console.log('Using pre-signed mini app metadata from environment');
return metadata;
} catch (error) {
console.warn(
"Failed to parse MINI_APP_METADATA from environment:",
error
'Failed to parse MINI_APP_METADATA from environment:',
error,
);
}
}
if (!APP_URL) {
throw new Error("NEXT_PUBLIC_URL not configured");
throw new Error('NEXT_PUBLIC_URL not configured');
}
// Get the domain from the URL (without https:// prefix)
const domain = new URL(APP_URL).hostname;
console.log("Using domain for manifest:", domain);
console.log('Using domain for manifest:', domain);
return {
accountAssociation: {
header: "",
payload: "",
signature: "",
header: '',
payload: '',
signature: '',
},
frame: {
version: "1",
name: APP_NAME ?? "Neynar Starter Kit",
version: '1',
name: APP_NAME ?? 'Neynar Starter Kit',
iconUrl: APP_ICON_URL,
homeUrl: APP_URL,
imageUrl: APP_OG_IMAGE_URL,
buttonTitle: APP_BUTTON_TEXT ?? "Launch Mini App",
buttonTitle: APP_BUTTON_TEXT ?? 'Launch Mini App',
splashImageUrl: APP_SPLASH_URL,
splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR,
webhookUrl: APP_WEBHOOK_URL,