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

@ -27,4 +27,4 @@ jobs:
run: npm ci run: npm ci
- name: Publish to npm - name: Publish to npm
run: npm publish --access public run: npm publish --access public

View File

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

View File

@ -1,19 +1,19 @@
#!/usr/bin/env node #!/usr/bin/env node
import inquirer from 'inquirer';
import { fileURLToPath } from 'url';
import { dirname } from 'path';
import { execSync } from 'child_process'; import { execSync } from 'child_process';
import fs from 'fs';
import path from 'path';
import crypto from 'crypto'; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git'; const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git';
const SCRIPT_VERSION = JSON.parse( const SCRIPT_VERSION = JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8') fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'),
).version; ).version;
// ANSI color codes // ANSI color codes
@ -47,12 +47,12 @@ async function queryNeynarApp(apiKey) {
} }
try { try {
const response = await fetch( 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: { headers: {
'x-api-key': apiKey, 'x-api-key': apiKey,
}, },
} },
); );
const data = await response.json(); const data = await response.json();
return data; return data;
@ -63,7 +63,11 @@ async function queryNeynarApp(apiKey) {
} }
// Export the main CLI function for programmatic use // 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(); printWelcomeMessage();
// Ask about Neynar usage // Ask about Neynar usage
@ -107,7 +111,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
} else { } else {
if (!autoAcceptDefaults) { if (!autoAcceptDefaults) {
console.log( 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) { if (useDemoKey.useDemo) {
console.warn( 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( 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( 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'; neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO';
} }
@ -163,7 +167,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
break; break;
} }
console.log( 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([ const { retry } = await inquirer.prompt([
{ {
@ -239,7 +243,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
name: 'projectName', name: 'projectName',
message: 'What is the name of your mini app?', message: 'What is the name of your mini app?',
default: projectName || defaultMiniAppName, default: projectName || defaultMiniAppName,
validate: (input) => { validate: input => {
if (input.trim() === '') { if (input.trim() === '') {
return 'Project name cannot be empty'; return 'Project name cannot be empty';
} }
@ -286,13 +290,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
message: message:
'Enter tags for your mini app (separate with spaces or commas, optional):', 'Enter tags for your mini app (separate with spaces or commas, optional):',
default: '', default: '',
filter: (input) => { filter: input => {
if (!input.trim()) return []; if (!input.trim()) return [];
// Split by both spaces and commas, trim whitespace, and filter out empty strings // Split by both spaces and commas, trim whitespace, and filter out empty strings
return input return input
.split(/[,\s]+/) .split(/[,\s]+/)
.map((tag) => tag.trim()) .map(tag => tag.trim())
.filter((tag) => tag.length > 0); .filter(tag => tag.length > 0);
}, },
}, },
{ {
@ -300,7 +304,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
name: 'buttonText', name: 'buttonText',
message: 'Enter the button text for your mini app:', message: 'Enter the button text for your mini app:',
default: 'Launch Mini App', default: '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';
} }
@ -370,8 +374,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
{ {
type: 'password', type: 'password',
name: 'seedPhrase', name: 'seedPhrase',
message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):', message:
validate: (input) => { 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):',
validate: input => {
if (!input || input.trim().split(' ').length < 12) { if (!input || input.trim().split(' ').length < 12) {
return 'Seed phrase must be at least 12 words'; 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 // Update package.json
console.log('\nUpdating package.json...'); console.log('\nUpdating package.json...');
const packageJsonPath = path.join(projectPath, '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.name = finalProjectName;
packageJson.version = '0.1.0'; packageJson.version = '0.1.0';
@ -522,21 +527,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
let constantsContent = fs.readFileSync(constantsPath, 'utf8'); let constantsContent = fs.readFileSync(constantsPath, 'utf8');
// Helper function to escape single quotes in strings // 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 // Helper function to safely replace constants with validation
const safeReplace = (content, pattern, replacement, constantName) => { const safeReplace = (content, pattern, replacement, constantName) => {
const match = content.match(pattern); const match = content.match(pattern);
if (!match) { if (!match) {
console.log( 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(`Pattern: ${pattern}`);
console.log( console.log(
`Expected to match in: ${ `Expected to match in: ${
content.split('\n').find((line) => line.includes(constantName)) || content.split('\n').find(line => line.includes(constantName)) ||
'Not found' 'Not found'
}` }`,
); );
} else { } else {
const newContent = content.replace(pattern, replacement); const newContent = content.replace(pattern, replacement);
@ -565,7 +570,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.APP_NAME, patterns.APP_NAME,
`export const APP_NAME = '${escapeString(answers.projectName)}';`, `export const APP_NAME = '${escapeString(answers.projectName)}';`,
'APP_NAME' 'APP_NAME',
); );
// Update APP_DESCRIPTION // Update APP_DESCRIPTION
@ -573,9 +578,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.APP_DESCRIPTION, patterns.APP_DESCRIPTION,
`export const APP_DESCRIPTION = '${escapeString( `export const APP_DESCRIPTION = '${escapeString(
answers.description answers.description,
)}';`, )}';`,
'APP_DESCRIPTION' 'APP_DESCRIPTION',
); );
// Update APP_PRIMARY_CATEGORY (always update, null becomes empty string) // Update APP_PRIMARY_CATEGORY (always update, null becomes empty string)
@ -583,21 +588,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.APP_PRIMARY_CATEGORY, patterns.APP_PRIMARY_CATEGORY,
`export const APP_PRIMARY_CATEGORY = '${escapeString( `export const APP_PRIMARY_CATEGORY = '${escapeString(
answers.primaryCategory || '' answers.primaryCategory || '',
)}';`, )}';`,
'APP_PRIMARY_CATEGORY' 'APP_PRIMARY_CATEGORY',
); );
// Update APP_TAGS // Update APP_TAGS
const tagsString = const tagsString =
answers.tags.length > 0 answers.tags.length > 0
? `['${answers.tags.map((tag) => escapeString(tag)).join("', '")}']` ? `['${answers.tags.map(tag => escapeString(tag)).join("', '")}']`
: "['neynar', 'starter-kit', 'demo']"; : "['neynar', 'starter-kit', 'demo']";
constantsContent = safeReplace( constantsContent = safeReplace(
constantsContent, constantsContent,
patterns.APP_TAGS, patterns.APP_TAGS,
`export const APP_TAGS = ${tagsString};`, `export const APP_TAGS = ${tagsString};`,
'APP_TAGS' 'APP_TAGS',
); );
// Update APP_BUTTON_TEXT (always update, use answers value) // Update APP_BUTTON_TEXT (always update, use answers value)
@ -605,9 +610,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.APP_BUTTON_TEXT, patterns.APP_BUTTON_TEXT,
`export const APP_BUTTON_TEXT = '${escapeString( `export const APP_BUTTON_TEXT = '${escapeString(
answers.buttonText || '' answers.buttonText || '',
)}';`, )}';`,
'APP_BUTTON_TEXT' 'APP_BUTTON_TEXT',
); );
// Update USE_WALLET // Update USE_WALLET
@ -615,7 +620,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.USE_WALLET, patterns.USE_WALLET,
`export const USE_WALLET = ${answers.useWallet};`, `export const USE_WALLET = ${answers.useWallet};`,
'USE_WALLET' 'USE_WALLET',
); );
// Update ANALYTICS_ENABLED // Update ANALYTICS_ENABLED
@ -623,7 +628,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
constantsContent, constantsContent,
patterns.ANALYTICS_ENABLED, patterns.ANALYTICS_ENABLED,
`export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`, `export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`,
'ANALYTICS_ENABLED' 'ANALYTICS_ENABLED',
); );
fs.writeFileSync(constantsPath, constantsContent); fs.writeFileSync(constantsPath, constantsContent);
@ -633,14 +638,14 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.appendFileSync( fs.appendFileSync(
envPath, envPath,
`\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"` `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`,
); );
if (useNeynar && neynarApiKey && neynarClientId) { if (useNeynar && neynarApiKey && neynarClientId) {
fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`); fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`);
fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`); fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`);
} else if (useNeynar) { } else if (useNeynar) {
console.log( 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) { if (answers.seedPhrase) {
@ -651,7 +656,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
fs.unlinkSync(envExamplePath); fs.unlinkSync(envExamplePath);
} else { } else {
console.log( 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 add .', { cwd: projectPath });
execSync( execSync(
'git commit -m "initial commit from @neynar/create-farcaster-mini-app"', 'git commit -m "initial commit from @neynar/create-farcaster-mini-app"',
{ cwd: projectPath } { cwd: projectPath },
); );
// Calculate border length based on message length // Calculate border length based on message length
@ -710,4 +715,4 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe
console.log('\nTo run the app:'); console.log('\nTo run the app:');
console.log(` cd ${finalProjectName}`); console.log(` cd ${finalProjectName}`);
console.log(' npm run dev\n'); console.log(' npm run dev\n');
} }

4
package-lock.json generated
View File

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

View File

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

View File

@ -1,12 +1,12 @@
import { execSync, spawn } from 'child_process'; 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 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 { Vercel } from '@vercel/sdk';
import dotenv from 'dotenv';
import inquirer from 'inquirer';
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.join(__dirname, '..'); const projectRoot = path.join(__dirname, '..');
@ -99,20 +99,19 @@ async function checkRequiredEnvVars() {
name: 'NEXT_PUBLIC_MINI_APP_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_MINI_APP_NAME, default: process.env.NEXT_PUBLIC_MINI_APP_NAME,
validate: (input) => validate: input => input.trim() !== '' || 'Mini app name cannot be empty',
input.trim() !== '' || 'Mini app name cannot be empty',
}, },
{ {
name: 'NEXT_PUBLIC_MINI_APP_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: default:
process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', 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( const missingVars = requiredVars.filter(
(varConfig) => !process.env[varConfig.name] varConfig => !process.env[varConfig.name],
); );
if (missingVars.length > 0) { if (missingVars.length > 0) {
@ -138,7 +137,7 @@ async function checkRequiredEnvVars() {
const newLine = envContent ? '\n' : ''; const newLine = envContent ? '\n' : '';
fs.appendFileSync( fs.appendFileSync(
'.env', '.env',
`${newLine}${varConfig.name}="${value.trim()}"` `${newLine}${varConfig.name}="${value.trim()}"`,
); );
} }
@ -161,7 +160,7 @@ async function checkRequiredEnvVars() {
if (storeSeedPhrase) { if (storeSeedPhrase) {
fs.appendFileSync( fs.appendFileSync(
'.env.local', '.env.local',
`\nSPONSOR_SIGNER="${sponsorSigner}"` `\nSPONSOR_SIGNER="${sponsorSigner}"`,
); );
console.log('✅ Sponsor signer preference stored in .env.local'); 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 return null; // We'll fall back to CLI operations
} catch (error) { } catch (error) {
throw new 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('3. Complete the Vercel account setup in your browser');
console.log('4. Return here once your Vercel account is created\n'); console.log('4. Return here once your Vercel account is created\n');
console.log( 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'], { const child = spawn('vercel', ['login'], {
@ -268,14 +267,14 @@ async function loginToVercel() {
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
child.on('close', (code) => { child.on('close', code => {
resolve(); resolve();
}); });
}); });
console.log('\n📱 Waiting for login to complete...'); console.log('\n📱 Waiting for login to complete...');
console.log( 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++) { for (let i = 0; i < 150; i++) {
@ -287,7 +286,7 @@ async function loginToVercel() {
if (error.message.includes('Account not found')) { if (error.message.includes('Account not found')) {
console.log(' Waiting for Vercel account setup to complete...'); 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( const existingVar = existingVars.envs?.find(
(env) => env.key === key && env.target?.includes('production') env => env.key === key && env.target?.includes('production'),
); );
if (existingVar) { if (existingVar) {
@ -345,7 +344,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) {
} catch (error) { } catch (error) {
console.warn( console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`, `⚠️ Warning: Failed to set environment variable ${key}:`,
error.message error.message,
); );
return false; return false;
} }
@ -400,7 +399,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) {
} }
console.warn( console.warn(
`⚠️ Warning: Failed to set environment variable ${key}:`, `⚠️ Warning: Failed to set environment variable ${key}:`,
error.message error.message,
); );
return false; return false;
} }
@ -410,7 +409,7 @@ async function setEnvironmentVariables(
vercelClient, vercelClient,
projectId, projectId,
envVars, envVars,
projectRoot projectRoot,
) { ) {
console.log('\n📝 Setting up environment variables...'); console.log('\n📝 Setting up environment variables...');
@ -435,12 +434,12 @@ async function setEnvironmentVariables(
} }
// Report results // Report results
const failed = results.filter((r) => !r.success); const failed = results.filter(r => !r.success);
if (failed.length > 0) { if (failed.length > 0) {
console.warn(`\n⚠️ Failed to set ${failed.length} environment variables:`); 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( 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( async function waitForDeployment(
vercelClient, vercelClient,
projectId, projectId,
maxWaitTime = 300000 maxWaitTime = 300000,
) { ) {
// 5 minutes // 5 minutes
console.log('\n⏳ Waiting for deployment to complete...'); console.log('\n⏳ Waiting for deployment to complete...');
@ -477,14 +476,14 @@ async function waitForDeployment(
} }
// Still building, wait and check again // 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 { } else {
console.log('⏳ No deployment found yet, waiting...'); console.log('⏳ No deployment found yet, waiting...');
await new Promise((resolve) => setTimeout(resolve, 5000)); await new Promise(resolve => setTimeout(resolve, 5000));
} }
} catch (error) { } catch (error) {
console.warn('⚠️ Could not check deployment status:', error.message); 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', framework: 'nextjs',
}, },
null, null,
2 2,
) ),
); );
} }
// Set up Vercel project // Set up Vercel project
console.log('\n📦 Setting up Vercel project...'); console.log('\n📦 Setting up Vercel project...');
console.log( 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( 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 // Use spawn instead of execSync for better error handling
@ -530,7 +529,7 @@ async function deployToVercel(useGitHub = false) {
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
vercelSetup.on('close', (code) => { vercelSetup.on('close', code => {
if (code === 0 || code === null) { if (code === 0 || code === null) {
console.log('✅ Vercel project setup completed'); console.log('✅ Vercel project setup completed');
resolve(); 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)'); console.log('⚠️ Vercel setup command completed (this is normal)');
resolve(); // Don't reject, as this is often expected resolve(); // Don't reject, as this is often expected
}); });
}); });
// Wait a moment for project files to be written // 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 // Load project info
let projectId; let projectId;
try { try {
const projectJson = JSON.parse( const projectJson = JSON.parse(
fs.readFileSync('.vercel/project.json', 'utf8') fs.readFileSync('.vercel/project.json', 'utf8'),
); );
projectId = projectJson.projectId; projectId = projectJson.projectId;
} catch (error) { } catch (error) {
throw new 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) { } catch (error) {
console.warn( 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); console.log('🌐 Using project name for domain:', domain);
} catch (error) { } catch (error) {
console.warn( 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, cwd: projectRoot,
encoding: 'utf8', encoding: 'utf8',
} },
); );
const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/);
@ -622,7 +621,7 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Using project name for domain:', domain); console.log('🌐 Using project name for domain:', domain);
} else { } else {
console.warn( 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 // Use a fallback domain based on project ID
domain = `project-${projectId.slice(-8)}.vercel.app`; domain = `project-${projectId.slice(-8)}.vercel.app`;
@ -670,8 +669,8 @@ async function deployToVercel(useGitHub = false) {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(process.env).filter(([key]) => Object.entries(process.env).filter(([key]) =>
key.startsWith('NEXT_PUBLIC_') key.startsWith('NEXT_PUBLIC_'),
) ),
), ),
}; };
@ -680,7 +679,7 @@ async function deployToVercel(useGitHub = false) {
vercelClient, vercelClient,
projectId, projectId,
vercelEnv, vercelEnv,
projectRoot projectRoot,
); );
// Deploy the project // Deploy the project
@ -704,7 +703,7 @@ async function deployToVercel(useGitHub = false) {
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
vercelDeploy.on('close', (code) => { vercelDeploy.on('close', code => {
if (code === 0) { if (code === 0) {
console.log('✅ Vercel deployment command completed'); console.log('✅ Vercel deployment command completed');
resolve(); 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); console.error('❌ Vercel deployment error:', error.message);
reject(error); reject(error);
}); });
@ -728,7 +727,7 @@ async function deployToVercel(useGitHub = false) {
} catch (error) { } catch (error) {
console.warn( console.warn(
'⚠️ Could not verify deployment completion:', '⚠️ Could not verify deployment completion:',
error.message error.message,
); );
console.log(' Proceeding with domain verification...'); console.log(' Proceeding with domain verification...');
} }
@ -744,7 +743,7 @@ async function deployToVercel(useGitHub = false) {
console.log('🌐 Verified actual domain:', actualDomain); console.log('🌐 Verified actual domain:', actualDomain);
} catch (error) { } catch (error) {
console.warn( 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, fid,
await validateSeedPhrase(process.env.SEED_PHRASE), await validateSeedPhrase(process.env.SEED_PHRASE),
process.env.SEED_PHRASE, process.env.SEED_PHRASE,
webhookUrl webhookUrl,
); );
updatedEnv.MINI_APP_METADATA = updatedMetadata; updatedEnv.MINI_APP_METADATA = updatedMetadata;
} }
@ -778,7 +777,7 @@ async function deployToVercel(useGitHub = false) {
vercelClient, vercelClient,
projectId, projectId,
updatedEnv, updatedEnv,
projectRoot projectRoot,
); );
console.log('\n📦 Redeploying with correct domain...'); console.log('\n📦 Redeploying with correct domain...');
@ -789,7 +788,7 @@ async function deployToVercel(useGitHub = false) {
}); });
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
vercelRedeploy.on('close', (code) => { vercelRedeploy.on('close', code => {
if (code === 0) { if (code === 0) {
console.log('✅ Redeployment completed'); console.log('✅ Redeployment completed');
resolve(); resolve();
@ -799,7 +798,7 @@ async function deployToVercel(useGitHub = false) {
} }
}); });
vercelRedeploy.on('error', (error) => { vercelRedeploy.on('error', error => {
console.error('❌ Redeployment error:', error.message); console.error('❌ Redeployment error:', error.message);
reject(error); 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('\n✨ Deployment complete! Your mini app is now live at:');
console.log(`🌐 https://${domain}`); console.log(`🌐 https://${domain}`);
console.log( 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) { } catch (error) {
console.error('\n❌ Deployment failed:', error.message); console.error('\n❌ Deployment failed:', error.message);
@ -823,7 +822,7 @@ async function main() {
try { try {
console.log('🚀 Vercel Mini App Deployment (SDK Edition)'); console.log('🚀 Vercel Mini App Deployment (SDK Edition)');
console.log( 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('\nThe script will:');
console.log('1. Check for required environment variables'); console.log('1. Check for required environment variables');
@ -902,4 +901,4 @@ async function main() {
} }
} }
main(); main();

View File

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

View File

@ -10,7 +10,7 @@ export async function GET(request: Request) {
if (!message || !signature) { if (!message || !signature) {
return NextResponse.json( return NextResponse.json(
{ error: 'Message and signature are required' }, { 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); console.error('Error in session-signers API:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch signers' }, { 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); console.error('Error fetching signer:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch signer' }, { error: 'Failed to fetch signer' },
{ status: 500 } { status: 500 },
); );
} }
} }
@ -22,7 +22,7 @@ export async function GET(request: Request) {
if (!signerUuid) { if (!signerUuid) {
return NextResponse.json( return NextResponse.json(
{ error: 'signerUuid is required' }, { 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); console.error('Error fetching signed key:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch signed key' }, { error: 'Failed to fetch signed key' },
{ status: 500 } { status: 500 },
); );
} }
} }

View File

@ -1,10 +1,10 @@
import { NextResponse } from 'next/server'; import { NextResponse } from 'next/server';
import { getNeynarClient } from '~/lib/neynar';
import { mnemonicToAccount } from 'viem/accounts'; import { mnemonicToAccount } from 'viem/accounts';
import { import {
SIGNED_KEY_REQUEST_TYPE, SIGNED_KEY_REQUEST_TYPE,
SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN,
} from '~/lib/constants'; } from '~/lib/constants';
import { getNeynarClient } from '~/lib/neynar';
const postRequiredFields = ['signerUuid', 'publicKey']; const postRequiredFields = ['signerUuid', 'publicKey'];
@ -16,7 +16,7 @@ export async function POST(request: Request) {
if (!body[field]) { if (!body[field]) {
return NextResponse.json( return NextResponse.json(
{ error: `${field} is required` }, { error: `${field} is required` },
{ status: 400 } { status: 400 },
); );
} }
} }
@ -26,7 +26,7 @@ export async function POST(request: Request) {
if (redirectUrl && typeof redirectUrl !== 'string') { if (redirectUrl && typeof redirectUrl !== 'string') {
return NextResponse.json( return NextResponse.json(
{ error: 'redirectUrl must be a string' }, { error: 'redirectUrl must be a string' },
{ status: 400 } { status: 400 },
); );
} }
@ -38,7 +38,7 @@ export async function POST(request: Request) {
if (!seedPhrase) { if (!seedPhrase) {
return NextResponse.json( return NextResponse.json(
{ error: 'App configuration missing (SEED_PHRASE or FID)' }, { 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); console.error('Error registering signed key:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to register signed key' }, { 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`, 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); console.error('Error fetching signers:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to fetch signers' }, { 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) { if (!session?.user?.fid) {
return NextResponse.json( return NextResponse.json(
{ error: 'No authenticated session found' }, { error: 'No authenticated session found' },
{ status: 401 } { status: 401 },
); );
} }
@ -19,7 +19,7 @@ export async function POST(request: Request) {
if (!signers || !user) { if (!signers || !user) {
return NextResponse.json( return NextResponse.json(
{ error: 'Signers and user are required' }, { 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); console.error('Error preparing session update:', error);
return NextResponse.json( return NextResponse.json(
{ error: 'Failed to prepare session update' }, { 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 { NextRequest } from "next/server"; import { notificationDetailsSchema } from '@farcaster/miniapp-sdk';
import { z } from "zod"; import { z } from 'zod';
import { setUserNotificationDetails } from "~/lib/kv"; import { setUserNotificationDetails } from '~/lib/kv';
import { sendMiniAppNotification } from "~/lib/notifs"; import { sendNeynarMiniAppNotification } from '~/lib/neynar';
import { sendNeynarMiniAppNotification } from "~/lib/neynar"; import { sendMiniAppNotification } from '~/lib/notifs';
const requestSchema = z.object({ const requestSchema = z.object({
fid: z.number(), fid: z.number(),
@ -13,7 +13,8 @@ const requestSchema = z.object({
export async function POST(request: NextRequest) { export async function POST(request: NextRequest) {
// If Neynar is enabled, we don't need to store notification details // If Neynar is enabled, we don't need to store notification details
// as they will be managed by Neynar's system // 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 requestJson = await request.json();
const requestBody = requestSchema.safeParse(requestJson); const requestBody = requestSchema.safeParse(requestJson);
@ -21,7 +22,7 @@ export async function POST(request: NextRequest) {
if (requestBody.success === false) { if (requestBody.success === false) {
return Response.json( return Response.json(
{ success: false, errors: requestBody.error.errors }, { success: false, errors: requestBody.error.errors },
{ status: 400 } { status: 400 },
); );
} }
@ -29,29 +30,31 @@ export async function POST(request: NextRequest) {
if (!neynarEnabled) { if (!neynarEnabled) {
await setUserNotificationDetails( await setUserNotificationDetails(
Number(requestBody.data.fid), Number(requestBody.data.fid),
requestBody.data.notificationDetails requestBody.data.notificationDetails,
); );
} }
// Use appropriate notification function based on Neynar status // Use appropriate notification function based on Neynar status
const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification; 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',
body: "Sent at " + new Date().toISOString(), body: 'Sent at ' + new Date().toISOString(),
}); });
if (sendResult.state === "error") { if (sendResult.state === 'error') {
return Response.json( return Response.json(
{ success: false, error: sendResult.error }, { success: false, error: sendResult.error },
{ status: 500 } { status: 500 },
); );
} else if (sendResult.state === "rate_limit") { } else if (sendResult.state === 'rate_limit') {
return Response.json( return Response.json(
{ success: false, error: "Rate limited" }, { success: false, error: 'Rate limited' },
{ status: 429 } { status: 429 },
); );
} }
return Response.json({ success: true }); return Response.json({ success: true });
} }

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { createAppClient, viemConnector } from '@farcaster/auth-client';
import { AuthOptions, getServerSession } from 'next-auth'; import { AuthOptions, getServerSession } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials'; import CredentialsProvider from 'next-auth/providers/credentials';
import { createAppClient, viemConnector } from '@farcaster/auth-client';
declare module 'next-auth' { declare module 'next-auth' {
interface Session { interface Session {
@ -401,7 +401,7 @@ export const authOptions: AuthOptions = {
}, },
cookies: { cookies: {
sessionToken: { sessionToken: {
name: `next-auth.session-token`, name: 'next-auth.session-token',
options: { options: {
httpOnly: true, httpOnly: true,
sameSite: 'none', sameSite: 'none',
@ -410,7 +410,7 @@ export const authOptions: AuthOptions = {
}, },
}, },
callbackUrl: { callbackUrl: {
name: `next-auth.callback-url`, name: 'next-auth.callback-url',
options: { options: {
sameSite: 'none', sameSite: 'none',
path: '/', path: '/',
@ -418,7 +418,7 @@ export const authOptions: AuthOptions = {
}, },
}, },
csrfToken: { csrfToken: {
name: `next-auth.csrf-token`, name: 'next-auth.csrf-token',
options: { options: {
httpOnly: true, httpOnly: true,
sameSite: 'none', sameSite: 'none',
@ -436,4 +436,4 @@ export const getSession = async () => {
console.error('Error getting server session:', error); console.error('Error getting server session:', error);
return null; return null;
} }
}; };

View File

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

View File

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

View File

@ -1,9 +1,9 @@
"use client"; 'use client';
import { useState } from "react"; import { useState } from 'react';
import { APP_NAME } from "~/lib/constants"; import sdk from '@farcaster/miniapp-sdk';
import sdk from "@farcaster/miniapp-sdk"; import { useMiniApp } from '@neynar/react';
import { useMiniApp } from "@neynar/react"; import { APP_NAME } from '~/lib/constants';
type HeaderProps = { type HeaderProps = {
neynarUser?: { neynarUser?: {
@ -18,23 +18,19 @@ export function Header({ neynarUser }: HeaderProps) {
return ( return (
<div className="relative"> <div className="relative">
<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">
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="text-lg font-light">
Welcome to {APP_NAME}!
</div>
{context?.user && ( {context?.user && (
<div <div
className="cursor-pointer" className="cursor-pointer"
onClick={() => { onClick={() => {
setIsUserDropdownOpen(!isUserDropdownOpen); setIsUserDropdownOpen(!isUserDropdownOpen);
}} }}
> >
{context.user.pfpUrl && ( {context.user.pfpUrl && (
<img <img
src={context.user.pfpUrl} src={context.user.pfpUrl}
alt="Profile" alt="Profile"
className="w-10 h-10 rounded-full border-2 border-primary" className="w-10 h-10 rounded-full border-2 border-primary"
/> />
)} )}
@ -42,14 +38,16 @@ export function Header({ neynarUser }: HeaderProps) {
)} )}
</div> </div>
{context?.user && ( {context?.user && (
<> <>
{isUserDropdownOpen && ( {isUserDropdownOpen && (
<div className="absolute top-full right-0 z-50 w-fit mt-1 mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700"> <div className="absolute top-full right-0 z-50 w-fit mt-1 mx-4 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700">
<div className="p-3 space-y-2"> <div className="p-3 space-y-2">
<div className="text-right"> <div className="text-right">
<h3 <h3
className="font-bold text-sm hover:underline cursor-pointer inline-block" 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} {context.user.displayName || context.user.username}
</h3> </h3>
@ -74,4 +72,4 @@ export function Header({ neynarUser }: HeaderProps) {
)} )}
</div> </div>
); );
} }

View File

@ -169,7 +169,7 @@ export function AuthDialog({
{/* eslint-disable-next-line @next/next/no-img-element */} {/* eslint-disable-next-line @next/next/no-img-element */}
<img <img
src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent( src={`https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${encodeURIComponent(
content.qrUrl content.qrUrl,
)}`} )}`}
alt="QR Code" alt="QR Code"
className="w-48 h-48" className="w-48 h-48"
@ -197,14 +197,14 @@ export function AuthDialog({
content.qrUrl content.qrUrl
.replace( .replace(
'https://farcaster.xyz/', 'https://farcaster.xyz/',
'https://client.farcaster.xyz/deeplinks/' 'https://client.farcaster.xyz/deeplinks/',
) )
.replace( .replace(
'https://client.farcaster.xyz/deeplinks/signed-key-request', '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" 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', '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', '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', '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 */} {/* eslint-disable-next-line @next/next/no-img-element */}
@ -35,7 +35,7 @@ export function ProfileButton({
src={pfpUrl} src={pfpUrl}
alt="Profile" alt="Profile"
className="w-6 h-6 rounded-full object-cover flex-shrink-0" className="w-6 h-6 rounded-full object-cover flex-shrink-0"
onError={(e) => { onError={e => {
(e.target as HTMLImageElement).src = (e.target as HTMLImageElement).src =
'https://farcaster.xyz/avatar.png'; 'https://farcaster.xyz/avatar.png';
}} }}
@ -46,7 +46,7 @@ export function ProfileButton({
<svg <svg
className={cn( className={cn(
'w-4 h-4 transition-transform flex-shrink-0', 'w-4 h-4 transition-transform flex-shrink-0',
showDropdown && 'rotate-180' showDropdown && 'rotate-180',
)} )}
fill="none" fill="none"
stroke="currentColor" stroke="currentColor"

View File

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

View File

@ -1,9 +1,9 @@
'use client'; 'use client';
import { useCallback, useState, useEffect } from 'react'; import { useCallback, useState, useEffect } from 'react';
import { Button } from './Button'; import { type ComposeCast } from '@farcaster/miniapp-sdk';
import { useMiniApp } from '@neynar/react'; import { useMiniApp } from '@neynar/react';
import { type ComposeCast } from "@farcaster/miniapp-sdk"; import { Button } from './Button';
interface EmbedConfig { interface EmbedConfig {
path?: string; path?: string;
@ -23,9 +23,16 @@ interface ShareButtonProps {
isLoading?: boolean; 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 [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 [isLoadingBestFriends, setIsLoadingBestFriends] = useState(false);
const { context, actions } = useMiniApp(); const { context, actions } = useMiniApp();
@ -51,7 +58,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
if (cast.bestFriends) { if (cast.bestFriends) {
if (bestFriends) { if (bestFriends) {
// Replace @N with usernames, or remove if no matching friend // 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 friendIndex = parseInt(match.slice(1)) - 1;
const friend = bestFriends[friendIndex]; const friend = bestFriends[friendIndex];
if (friend) { if (friend) {
@ -67,16 +74,20 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
// Process embeds // Process embeds
const processedEmbeds = await Promise.all( const processedEmbeds = await Promise.all(
(cast.embeds || []).map(async (embed) => { (cast.embeds || []).map(async embed => {
if (typeof embed === 'string') { if (typeof embed === 'string') {
return embed; return embed;
} }
if (embed.path) { 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}`); const url = new URL(`${baseUrl}${embed.path}`);
// Add UTM parameters // 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 custom image generator is provided, use it
if (embed.imageUrl) { if (embed.imageUrl) {
@ -87,7 +98,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
return url.toString(); return url.toString();
} }
return embed.url || ''; return embed.url || '';
}) }),
); );
// Open cast composer with all supported intents // Open cast composer with all supported intents
@ -115,4 +126,4 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals
{buttonText} {buttonText}
</Button> </Button>
); );
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,18 @@
import { import {
SendNotificationRequest, SendNotificationRequest,
sendNotificationResponseSchema, sendNotificationResponseSchema,
} from "@farcaster/miniapp-sdk"; } from '@farcaster/miniapp-sdk';
import { getUserNotificationDetails } from "~/lib/kv"; import { getUserNotificationDetails } from '~/lib/kv';
import { APP_URL } from "./constants"; import { APP_URL } from './constants';
type SendMiniAppNotificationResult = type SendMiniAppNotificationResult =
| { | {
state: "error"; state: 'error';
error: unknown; error: unknown;
} }
| { state: "no_token" } | { state: 'no_token' }
| { state: "rate_limit" } | { state: 'rate_limit' }
| { state: "success" }; | { state: 'success' };
export async function sendMiniAppNotification({ export async function sendMiniAppNotification({
fid, fid,
@ -25,13 +25,13 @@ export async function sendMiniAppNotification({
}): Promise<SendMiniAppNotificationResult> { }): Promise<SendMiniAppNotificationResult> {
const notificationDetails = await getUserNotificationDetails(fid); const notificationDetails = await getUserNotificationDetails(fid);
if (!notificationDetails) { if (!notificationDetails) {
return { state: "no_token" }; return { state: 'no_token' };
} }
const response = await fetch(notificationDetails.url, { const response = await fetch(notificationDetails.url, {
method: "POST", method: 'POST',
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
body: JSON.stringify({ body: JSON.stringify({
notificationId: crypto.randomUUID(), notificationId: crypto.randomUUID(),
@ -48,17 +48,17 @@ export async function sendMiniAppNotification({
const responseBody = sendNotificationResponseSchema.safeParse(responseJson); const responseBody = sendNotificationResponseSchema.safeParse(responseJson);
if (responseBody.success === false) { if (responseBody.success === false) {
// Malformed response // Malformed response
return { state: "error", error: responseBody.error.errors }; return { state: 'error', error: responseBody.error.errors };
} }
if (responseBody.data.result.rateLimitedTokens.length) { if (responseBody.data.result.rateLimitedTokens.length) {
// Rate limited // Rate limited
return { state: "rate_limit" }; return { state: 'rate_limit' };
} }
return { state: "success" }; return { state: 'success' };
} else { } else {
// Error response // 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 { type ClassValue, clsx } from 'clsx';
import { twMerge } from "tailwind-merge"; import { twMerge } from 'tailwind-merge';
import { mnemonicToAccount } from "viem/accounts";
import { import {
APP_BUTTON_TEXT, APP_BUTTON_TEXT,
APP_DESCRIPTION, APP_DESCRIPTION,
@ -12,8 +11,8 @@ import {
APP_TAGS, APP_TAGS,
APP_URL, APP_URL,
APP_WEBHOOK_URL, APP_WEBHOOK_URL,
} from "./constants"; } from './constants';
import { APP_SPLASH_URL } from "./constants"; import { APP_SPLASH_URL } from './constants';
interface MiniAppMetadata { interface MiniAppMetadata {
version: string; version: string;
@ -45,12 +44,12 @@ export function cn(...inputs: ClassValue[]) {
export function getMiniAppEmbedMetadata(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,
button: { button: {
title: APP_BUTTON_TEXT, title: APP_BUTTON_TEXT,
action: { action: {
type: "launch_frame", type: 'launch_frame',
name: APP_NAME, name: APP_NAME,
url: APP_URL, url: APP_URL,
splashImageUrl: APP_SPLASH_URL, splashImageUrl: APP_SPLASH_URL,
@ -69,37 +68,37 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
if (process.env.MINI_APP_METADATA) { if (process.env.MINI_APP_METADATA) {
try { try {
const metadata = JSON.parse(process.env.MINI_APP_METADATA); 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; return metadata;
} catch (error) { } catch (error) {
console.warn( console.warn(
"Failed to parse MINI_APP_METADATA from environment:", 'Failed to parse MINI_APP_METADATA from environment:',
error error,
); );
} }
} }
if (!APP_URL) { 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) // Get the domain from the URL (without https:// prefix)
const domain = new URL(APP_URL).hostname; const domain = new URL(APP_URL).hostname;
console.log("Using domain for manifest:", domain); console.log('Using domain for manifest:', domain);
return { return {
accountAssociation: { accountAssociation: {
header: "", header: '',
payload: "", payload: '',
signature: "", signature: '',
}, },
frame: { frame: {
version: "1", version: '1',
name: APP_NAME ?? "Neynar Starter Kit", 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 Mini App", 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,
@ -108,4 +107,4 @@ export async function getFarcasterMetadata(): Promise<MiniAppManifest> {
tags: APP_TAGS, tags: APP_TAGS,
}, },
}; };
} }