From e74b2581df177ca2a59325d8667328ac94669047 Mon Sep 17 00:00:00 2001 From: Shreyaschorge Date: Mon, 14 Jul 2025 20:04:44 +0530 Subject: [PATCH] Format after fixing conflicts --- .github/workflows/publish.yml | 2 +- bin/index.js | 62 +++--- bin/init.js | 89 ++++----- package-lock.json | 4 +- scripts/build.js | 178 +++++++++--------- scripts/deploy.js | 109 ++++++----- src/app/api/auth/nonce/route.ts | 2 +- src/app/api/auth/session-signers/route.ts | 4 +- src/app/api/auth/signer/route.ts | 6 +- src/app/api/auth/signer/signed_key/route.ts | 10 +- src/app/api/auth/signers/route.ts | 4 +- src/app/api/auth/update-session/route.ts | 6 +- src/app/api/send-notification/route.ts | 39 ++-- src/app/api/webhook/route.ts | 41 ++-- src/app/providers.tsx | 8 +- src/auth.ts | 10 +- .../providers/SafeFarcasterSolanaProvider.tsx | 28 ++- src/components/providers/WagmiProvider.tsx | 35 ++-- src/components/ui/Header.tsx | 36 ++-- .../ui/NeynarAuthButton/AuthDialog.tsx | 10 +- .../ui/NeynarAuthButton/ProfileButton.tsx | 6 +- src/components/ui/NeynarAuthButton/index.tsx | 73 +++---- src/components/ui/Share.tsx | 31 ++- src/components/ui/tabs/ActionsTab.tsx | 58 +++--- src/components/ui/tabs/HomeTab.tsx | 4 +- src/components/ui/wallet/SignIn.tsx | 36 ++-- src/hooks/useDetectClickOutside.ts | 2 +- src/lib/kv.ts | 24 +-- src/lib/notifs.ts | 30 +-- src/lib/utils.ts | 37 ++-- 30 files changed, 515 insertions(+), 469 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ddc32ca..3151fb5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,4 +27,4 @@ jobs: run: npm ci - name: Publish to npm - run: npm publish --access public \ No newline at end of file + run: npm publish --access public diff --git a/bin/index.js b/bin/index.js index 65ec3e0..d1d0bc5 100755 --- a/bin/index.js +++ b/bin/index.js @@ -15,48 +15,48 @@ if (yIndex !== -1) { args.splice(yIndex, 1); // Remove -y from args } - // Parse other arguments - for (let i = 0; i < args.length; i++) { - const arg = args[i]; - - if (arg === '-p' || arg === '--project') { - if (i + 1 < args.length) { - projectName = args[i + 1]; - if (projectName.startsWith('-')) { - console.error('Error: Project name cannot start with a dash (-)'); - process.exit(1); - } - args.splice(i, 2); // Remove both the flag and its value - i--; // Adjust index since we removed 2 elements - } else { - console.error('Error: -p/--project requires a project name'); +// Parse other arguments +for (let i = 0; i < args.length; i++) { + const arg = args[i]; + + if (arg === '-p' || arg === '--project') { + if (i + 1 < args.length) { + projectName = args[i + 1]; + if (projectName.startsWith('-')) { + console.error('Error: Project name cannot start with a dash (-)'); process.exit(1); } - } else if (arg === '-k' || arg === '--api-key') { - if (i + 1 < args.length) { - apiKey = args[i + 1]; - if (apiKey.startsWith('-')) { - console.error('Error: API key cannot start with a dash (-)'); - process.exit(1); - } - args.splice(i, 2); // Remove both the flag and its value - i--; // Adjust index since we removed 2 elements - } else { - console.error('Error: -k/--api-key requires an API key'); + args.splice(i, 2); // Remove both the flag and its value + i--; // Adjust index since we removed 2 elements + } else { + console.error('Error: -p/--project requires a project name'); + process.exit(1); + } + } else if (arg === '-k' || arg === '--api-key') { + if (i + 1 < args.length) { + apiKey = args[i + 1]; + if (apiKey.startsWith('-')) { + console.error('Error: API key cannot start with a dash (-)'); process.exit(1); } + args.splice(i, 2); // Remove both the flag and its value + i--; // Adjust index since we removed 2 elements + } else { + console.error('Error: -k/--api-key requires an API key'); + process.exit(1); } } - - +} // Validate that if -y is used, a project name must be provided if (autoAcceptDefaults && !projectName) { - console.error('Error: -y flag requires a project name. Use -p/--project to specify the project name.'); + console.error( + 'Error: -y flag requires a project name. Use -p/--project to specify the project name.', + ); process.exit(1); } -init(projectName, autoAcceptDefaults, apiKey).catch((err) => { +init(projectName, autoAcceptDefaults, apiKey).catch(err => { console.error('Error:', err); process.exit(1); -}); \ No newline at end of file +}); diff --git a/bin/init.js b/bin/init.js index e1ddaa2..16cb38e 100644 --- a/bin/init.js +++ b/bin/init.js @@ -1,19 +1,19 @@ #!/usr/bin/env node -import inquirer from 'inquirer'; -import { fileURLToPath } from 'url'; -import { dirname } from 'path'; import { execSync } from 'child_process'; -import fs from 'fs'; -import path from 'path'; import crypto from 'crypto'; +import fs from 'fs'; +import { dirname } from 'path'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import inquirer from 'inquirer'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); const REPO_URL = 'https://github.com/neynarxyz/create-farcaster-mini-app.git'; const SCRIPT_VERSION = JSON.parse( - fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8') + fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8'), ).version; // ANSI color codes @@ -47,12 +47,12 @@ async function queryNeynarApp(apiKey) { } try { const response = await fetch( - `https://api.neynar.com/portal/app_by_api_key?starter_kit=true`, + 'https://api.neynar.com/portal/app_by_api_key?starter_kit=true', { headers: { 'x-api-key': apiKey, }, - } + }, ); const data = await response.json(); return data; @@ -63,7 +63,11 @@ async function queryNeynarApp(apiKey) { } // Export the main CLI function for programmatic use -export async function init(projectName = null, autoAcceptDefaults = false, apiKey = null) { +export async function init( + projectName = null, + autoAcceptDefaults = false, + apiKey = null, +) { printWelcomeMessage(); // Ask about Neynar usage @@ -107,7 +111,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe } else { if (!autoAcceptDefaults) { console.log( - '\nđŸĒ Find your Neynar API key at: https://dev.neynar.com/app\n' + '\nđŸĒ Find your Neynar API key at: https://dev.neynar.com/app\n', ); } @@ -144,13 +148,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe if (useDemoKey.useDemo) { console.warn( - '\nâš ī¸ Note: the demo key is for development purposes only and is aggressively rate limited.' + '\nâš ī¸ Note: the demo key is for development purposes only and is aggressively rate limited.', ); console.log( - 'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.' + 'For production, please sign up for a Neynar account at https://neynar.com/ and configure the API key in your .env or .env.local file with NEYNAR_API_KEY.', ); console.log( - `\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}` + `\n${purple}${bright}${italic}Neynar now has a free tier! See https://neynar.com/#pricing for details.\n${reset}`, ); neynarApiKey = 'FARCASTER_V2_FRAMES_DEMO'; } @@ -163,7 +167,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe break; } console.log( - '\nâš ī¸ No valid API key provided. Would you like to try again?' + '\nâš ī¸ No valid API key provided. Would you like to try again?', ); const { retry } = await inquirer.prompt([ { @@ -239,7 +243,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe name: 'projectName', message: 'What is the name of your mini app?', default: projectName || defaultMiniAppName, - validate: (input) => { + validate: input => { if (input.trim() === '') { return 'Project name cannot be empty'; } @@ -286,13 +290,13 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe message: 'Enter tags for your mini app (separate with spaces or commas, optional):', default: '', - filter: (input) => { + filter: input => { if (!input.trim()) return []; // Split by both spaces and commas, trim whitespace, and filter out empty strings return input .split(/[,\s]+/) - .map((tag) => tag.trim()) - .filter((tag) => tag.length > 0); + .map(tag => tag.trim()) + .filter(tag => tag.length > 0); }, }, { @@ -300,7 +304,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe name: 'buttonText', message: 'Enter the button text for your mini app:', default: 'Launch Mini App', - validate: (input) => { + validate: input => { if (input.trim() === '') { return 'Button text cannot be empty'; } @@ -370,8 +374,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe { type: 'password', name: 'seedPhrase', - message: 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):', - validate: (input) => { + message: + 'Enter your Farcaster custody account seed phrase (required for Neynar Sponsored Signers/SIWN):', + validate: input => { if (!input || input.trim().split(' ').length < 12) { return 'Seed phrase must be at least 12 words'; } @@ -439,7 +444,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe // Update package.json console.log('\nUpdating package.json...'); const packageJsonPath = path.join(projectPath, 'package.json'); - let packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); packageJson.name = finalProjectName; packageJson.version = '0.1.0'; @@ -522,21 +527,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe let constantsContent = fs.readFileSync(constantsPath, 'utf8'); // Helper function to escape single quotes in strings - const escapeString = (str) => str.replace(/'/g, "\\'"); + const escapeString = str => str.replace(/'/g, "\\'"); // Helper function to safely replace constants with validation const safeReplace = (content, pattern, replacement, constantName) => { const match = content.match(pattern); if (!match) { console.log( - `âš ī¸ Warning: Could not update ${constantName} in constants.ts. Pattern not found.` + `âš ī¸ Warning: Could not update ${constantName} in constants.ts. Pattern not found.`, ); console.log(`Pattern: ${pattern}`); console.log( `Expected to match in: ${ - content.split('\n').find((line) => line.includes(constantName)) || + content.split('\n').find(line => line.includes(constantName)) || 'Not found' - }` + }`, ); } else { const newContent = content.replace(pattern, replacement); @@ -565,7 +570,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.APP_NAME, `export const APP_NAME = '${escapeString(answers.projectName)}';`, - 'APP_NAME' + 'APP_NAME', ); // Update APP_DESCRIPTION @@ -573,9 +578,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.APP_DESCRIPTION, `export const APP_DESCRIPTION = '${escapeString( - answers.description + answers.description, )}';`, - 'APP_DESCRIPTION' + 'APP_DESCRIPTION', ); // Update APP_PRIMARY_CATEGORY (always update, null becomes empty string) @@ -583,21 +588,21 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.APP_PRIMARY_CATEGORY, `export const APP_PRIMARY_CATEGORY = '${escapeString( - answers.primaryCategory || '' + answers.primaryCategory || '', )}';`, - 'APP_PRIMARY_CATEGORY' + 'APP_PRIMARY_CATEGORY', ); // Update APP_TAGS const tagsString = answers.tags.length > 0 - ? `['${answers.tags.map((tag) => escapeString(tag)).join("', '")}']` + ? `['${answers.tags.map(tag => escapeString(tag)).join("', '")}']` : "['neynar', 'starter-kit', 'demo']"; constantsContent = safeReplace( constantsContent, patterns.APP_TAGS, `export const APP_TAGS = ${tagsString};`, - 'APP_TAGS' + 'APP_TAGS', ); // Update APP_BUTTON_TEXT (always update, use answers value) @@ -605,9 +610,9 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.APP_BUTTON_TEXT, `export const APP_BUTTON_TEXT = '${escapeString( - answers.buttonText || '' + answers.buttonText || '', )}';`, - 'APP_BUTTON_TEXT' + 'APP_BUTTON_TEXT', ); // Update USE_WALLET @@ -615,7 +620,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.USE_WALLET, `export const USE_WALLET = ${answers.useWallet};`, - 'USE_WALLET' + 'USE_WALLET', ); // Update ANALYTICS_ENABLED @@ -623,7 +628,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe constantsContent, patterns.ANALYTICS_ENABLED, `export const ANALYTICS_ENABLED = ${answers.enableAnalytics};`, - 'ANALYTICS_ENABLED' + 'ANALYTICS_ENABLED', ); fs.writeFileSync(constantsPath, constantsContent); @@ -633,14 +638,14 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.appendFileSync( envPath, - `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"` + `\nNEXTAUTH_SECRET="${crypto.randomBytes(32).toString('hex')}"`, ); if (useNeynar && neynarApiKey && neynarClientId) { fs.appendFileSync(envPath, `\nNEYNAR_API_KEY="${neynarApiKey}"`); fs.appendFileSync(envPath, `\nNEYNAR_CLIENT_ID="${neynarClientId}"`); } else if (useNeynar) { console.log( - '\nâš ī¸ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID' + '\nâš ī¸ Could not find a Neynar client ID and/or API key. Please configure Neynar manually in .env.local with NEYNAR_API_KEY and NEYNAR_CLIENT_ID', ); } if (answers.seedPhrase) { @@ -651,7 +656,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe fs.unlinkSync(envExamplePath); } else { console.log( - '\n.env.example does not exist, skipping copy and remove operations' + '\n.env.example does not exist, skipping copy and remove operations', ); } @@ -696,7 +701,7 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe execSync('git add .', { cwd: projectPath }); execSync( 'git commit -m "initial commit from @neynar/create-farcaster-mini-app"', - { cwd: projectPath } + { cwd: projectPath }, ); // Calculate border length based on message length @@ -710,4 +715,4 @@ export async function init(projectName = null, autoAcceptDefaults = false, apiKe console.log('\nTo run the app:'); console.log(` cd ${finalProjectName}`); console.log(' npm run dev\n'); -} \ No newline at end of file +} diff --git a/package-lock.json b/package-lock.json index 4cb6b82..eb78802 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.3", + "version": "1.5.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@neynar/create-farcaster-mini-app", - "version": "1.5.3", + "version": "1.5.9", "dependencies": { "dotenv": "^16.4.7", "inquirer": "^12.4.3", diff --git a/scripts/build.js b/scripts/build.js index 136a520..f6584b9 100755 --- a/scripts/build.js +++ b/scripts/build.js @@ -1,36 +1,37 @@ -import { execSync } from "child_process"; -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import inquirer from "inquirer"; -import dotenv from "dotenv"; -import crypto from "crypto"; +import { execSync } from 'child_process'; +import crypto from 'crypto'; +import fs from 'fs'; +import path from 'path'; +import { fileURLToPath } from 'url'; +import dotenv from 'dotenv'; +import inquirer from 'inquirer'; // Load environment variables in specific order // First load .env for main config -dotenv.config({ path: ".env" }); +dotenv.config({ path: '.env' }); async function loadEnvLocal() { try { - if (fs.existsSync(".env.local")) { + if (fs.existsSync('.env.local')) { const { loadLocal } = await inquirer.prompt([ { - type: "confirm", - name: "loadLocal", + type: 'confirm', + name: 'loadLocal', message: - "Found .env.local, likely created by the install script - would you like to load its values?", + 'Found .env.local, likely created by the install script - would you like to load its values?', default: false, }, ]); + const localEnv = dotenv.parse(fs.readFileSync('.env.local')); + if (loadLocal) { - console.log("Loading values from .env.local..."); - const localEnv = dotenv.parse(fs.readFileSync(".env.local")); + console.log('Loading values from .env.local...'); // Copy all values to .env - const envContent = fs.existsSync(".env") - ? fs.readFileSync(".env", "utf8") + "\n" - : ""; + const envContent = fs.existsSync('.env') + ? fs.readFileSync('.env', 'utf8') + '\n' + : ''; let newEnvContent = envContent; for (const [key, value] of Object.entries(localEnv)) { @@ -43,8 +44,8 @@ async function loadEnvLocal() { } // Write updated content to .env - fs.writeFileSync(".env", newEnvContent); - console.log("✅ Values from .env.local have been written to .env"); + fs.writeFileSync('.env', newEnvContent); + console.log('✅ Values from .env.local have been written to .env'); } if (localEnv.SPONSOR_SIGNER) { process.env.SPONSOR_SIGNER = localEnv.SPONSOR_SIGNER; @@ -52,26 +53,26 @@ async function loadEnvLocal() { } } catch (error) { // Error reading .env.local, which is fine - console.log("Note: No .env.local file found"); + console.log('Note: No .env.local file found'); } } // TODO: make sure rebuilding is supported const __dirname = path.dirname(fileURLToPath(import.meta.url)); -const projectRoot = path.join(__dirname, ".."); +const projectRoot = path.join(__dirname, '..'); async function validateDomain(domain) { // Remove http:// or https:// if present - const cleanDomain = domain.replace(/^https?:\/\//, ""); + const cleanDomain = domain.replace(/^https?:\/\//, ''); // Basic domain validation if ( !cleanDomain.match( - /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/ + /^[a-zA-Z0-9][a-zA-Z0-9-]{1,61}[a-zA-Z0-9](?:\.[a-zA-Z]{2,})+$/, ) ) { - throw new Error("Invalid domain format"); + throw new Error('Invalid domain format'); } return cleanDomain; @@ -83,39 +84,39 @@ async function queryNeynarApp(apiKey) { } try { const response = await fetch( - `https://api.neynar.com/portal/app_by_api_key`, + 'https://api.neynar.com/portal/app_by_api_key', { headers: { - "x-api-key": apiKey, + 'x-api-key': apiKey, }, - } + }, ); const data = await response.json(); return data; } catch (error) { - console.error("Error querying Neynar app data:", error); + console.error('Error querying Neynar app data:', error); return null; } } async function generateFarcasterMetadata(domain, webhookUrl) { - const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(","); + const tags = process.env.NEXT_PUBLIC_MINI_APP_TAGS?.split(','); return { accountAssociation: { - header: "", - payload: "", - signature: "", + header: '', + payload: '', + signature: '', }, frame: { - version: "1", + version: '1', name: process.env.NEXT_PUBLIC_MINI_APP_NAME, iconUrl: `https://${domain}/icon.png`, homeUrl: `https://${domain}`, imageUrl: `https://${domain}/api/opengraph-image`, buttonTitle: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT, splashImageUrl: `https://${domain}/splash.png`, - splashBackgroundColor: "#f7f7f7", + splashBackgroundColor: '#f7f7f7', webhookUrl, description: process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION, primaryCategory: process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY, @@ -126,8 +127,8 @@ async function generateFarcasterMetadata(domain, webhookUrl) { async function main() { try { - console.log("\n📝 Checking environment variables..."); - console.log("Loading values from .env..."); + console.log('\n📝 Checking environment variables...'); + console.log('Loading values from .env...'); // Load .env.local if user wants to await loadEnvLocal(); @@ -135,11 +136,11 @@ async function main() { // Get domain from user const { domain } = await inquirer.prompt([ { - type: "input", - name: "domain", + type: 'input', + name: 'domain', message: - "Enter the domain where your mini app will be deployed (e.g., example.com):", - validate: async (input) => { + 'Enter the domain where your mini app will be deployed (e.g., example.com):', + validate: async input => { try { await validateDomain(input); return true; @@ -153,13 +154,13 @@ async function main() { // Get frame name from user const { frameName } = await inquirer.prompt([ { - type: "input", - name: "frameName", - message: "Enter the name for your mini app (e.g., My Cool Mini App):", + type: 'input', + name: 'frameName', + message: 'Enter the name for your mini app (e.g., My Cool Mini App):', default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: (input) => { - if (input.trim() === "") { - return "Mini app name cannot be empty"; + validate: input => { + if (input.trim() === '') { + return 'Mini app name cannot be empty'; } return true; }, @@ -169,14 +170,14 @@ async function main() { // Get button text from user const { buttonText } = await inquirer.prompt([ { - type: "input", - name: "buttonText", - message: "Enter the text for your mini app button:", + type: 'input', + name: 'buttonText', + message: 'Enter the text for your mini app button:', default: - process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || "Launch Mini App", - validate: (input) => { - if (input.trim() === "") { - return "Button text cannot be empty"; + process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT || 'Launch Mini App', + validate: input => { + if (input.trim() === '') { + return 'Button text cannot be empty'; } return true; }, @@ -192,16 +193,16 @@ async function main() { if (!neynarApiKey) { const { neynarApiKey: inputNeynarApiKey } = await inquirer.prompt([ { - type: "password", - name: "neynarApiKey", + type: 'password', + name: 'neynarApiKey', message: - "Enter your Neynar API key (optional - leave blank to skip):", + 'Enter your Neynar API key (optional - leave blank to skip):', default: null, }, ]); neynarApiKey = inputNeynarApiKey; } else { - console.log("Using existing Neynar API key from .env"); + console.log('Using existing Neynar API key from .env'); } if (!neynarApiKey) { @@ -214,7 +215,7 @@ async function main() { const appInfo = await queryNeynarApp(neynarApiKey); if (appInfo) { neynarClientId = appInfo.app_uuid; - console.log("✅ Fetched Neynar app client ID"); + console.log('✅ Fetched Neynar app client ID'); break; } } @@ -226,13 +227,13 @@ async function main() { // If we get here, the API key was invalid console.log( - "\nâš ī¸ Could not find Neynar app information. The API key may be incorrect." + '\nâš ī¸ Could not find Neynar app information. The API key may be incorrect.', ); const { retry } = await inquirer.prompt([ { - type: "confirm", - name: "retry", - message: "Would you like to try a different API key?", + type: 'confirm', + name: 'retry', + message: 'Would you like to try a different API key?', default: true, }, ]); @@ -248,7 +249,7 @@ async function main() { } // Generate manifest - console.log("\n🔨 Generating mini app manifest..."); + console.log('\n🔨 Generating mini app manifest...'); // Determine webhook URL based on environment variables const webhookUrl = @@ -257,13 +258,13 @@ async function main() { : `https://${domain}/api/webhook`; const metadata = await generateFarcasterMetadata(domain, webhookUrl); - console.log("\n✅ Mini app manifest generated"); + console.log('\n✅ Mini app manifest generated'); // Read existing .env file or create new one - const envPath = path.join(projectRoot, ".env"); + const envPath = path.join(projectRoot, '.env'); let envContent = fs.existsSync(envPath) - ? fs.readFileSync(envPath, "utf8") - : ""; + ? fs.readFileSync(envPath, 'utf8') + : ''; // Add or update environment variables const newEnvVars = [ @@ -273,19 +274,19 @@ async function main() { // Mini app metadata `NEXT_PUBLIC_MINI_APP_NAME="${frameName}"`, `NEXT_PUBLIC_MINI_APP_DESCRIPTION="${ - process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || "" + process.env.NEXT_PUBLIC_MINI_APP_DESCRIPTION || '' }"`, `NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY="${ - process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || "" + process.env.NEXT_PUBLIC_MINI_APP_PRIMARY_CATEGORY || '' }"`, `NEXT_PUBLIC_MINI_APP_TAGS="${ - process.env.NEXT_PUBLIC_MINI_APP_TAGS || "" + process.env.NEXT_PUBLIC_MINI_APP_TAGS || '' }"`, `NEXT_PUBLIC_MINI_APP_BUTTON_TEXT="${buttonText}"`, // Analytics `NEXT_PUBLIC_ANALYTICS_ENABLED="${ - process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || "false" + process.env.NEXT_PUBLIC_ANALYTICS_ENABLED || 'false' }"`, // Neynar configuration (if it exists in current env) @@ -293,18 +294,19 @@ async function main() { ? [`NEYNAR_API_KEY="${process.env.NEYNAR_API_KEY}"`] : []), ...(neynarClientId ? [`NEYNAR_CLIENT_ID="${neynarClientId}"`] : []), - ...(process.env.SPONSOR_SIGNER ? - [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] : []), + ...(process.env.SPONSOR_SIGNER + ? [`SPONSOR_SIGNER="${process.env.SPONSOR_SIGNER}"`] + : []), // FID (if it exists in current env) ...(process.env.FID ? [`FID="${process.env.FID}"`] : []), `NEXT_PUBLIC_USE_WALLET="${ - process.env.NEXT_PUBLIC_USE_WALLET || "false" + process.env.NEXT_PUBLIC_USE_WALLET || 'false' }"`, // NextAuth configuration `NEXTAUTH_SECRET="${ - process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString("hex") + process.env.NEXTAUTH_SECRET || crypto.randomBytes(32).toString('hex') }"`, `NEXTAUTH_URL="https://${domain}"`, @@ -313,14 +315,14 @@ async function main() { ]; // Filter out empty values and join with newlines - const validEnvVars = newEnvVars.filter((line) => { - const [, value] = line.split("="); + const validEnvVars = newEnvVars.filter(line => { + const [, value] = line.split('='); return value && value !== '""'; }); // Update or append each environment variable - validEnvVars.forEach((varLine) => { - const [key] = varLine.split("="); + validEnvVars.forEach(varLine => { + const [key] = varLine.split('='); if (envContent.includes(`${key}=`)) { envContent = envContent.replace(new RegExp(`${key}=.*`), varLine); } else { @@ -331,29 +333,29 @@ async function main() { // Write updated .env file fs.writeFileSync(envPath, envContent); - console.log("\n✅ Environment variables updated"); + console.log('\n✅ Environment variables updated'); // Run next build - console.log("\nBuilding Next.js application..."); + console.log('\nBuilding Next.js application...'); const nextBin = path.normalize( - path.join(projectRoot, "node_modules", ".bin", "next") + path.join(projectRoot, 'node_modules', '.bin', 'next'), ); execSync(`"${nextBin}" build`, { cwd: projectRoot, - stdio: "inherit", - shell: process.platform === "win32", + stdio: 'inherit', + shell: process.platform === 'win32', }); console.log( - "\n✨ Build complete! Your mini app is ready for deployment. đŸĒ" + '\n✨ Build complete! Your mini app is ready for deployment. đŸĒ', ); console.log( - "📝 Make sure to configure the environment variables from .env in your hosting provider" + '📝 Make sure to configure the environment variables from .env in your hosting provider', ); } catch (error) { - console.error("\n❌ Error:", error.message); + console.error('\n❌ Error:', error.message); process.exit(1); } } -main(); \ No newline at end of file +main(); diff --git a/scripts/deploy.js b/scripts/deploy.js index b291146..78e4241 100755 --- a/scripts/deploy.js +++ b/scripts/deploy.js @@ -1,12 +1,12 @@ import { execSync, spawn } from 'child_process'; -import fs from 'fs'; -import path from 'path'; -import os from 'os'; -import { fileURLToPath } from 'url'; -import inquirer from 'inquirer'; -import dotenv from 'dotenv'; import crypto from 'crypto'; +import fs from 'fs'; +import os from 'os'; +import path from 'path'; +import { fileURLToPath } from 'url'; import { Vercel } from '@vercel/sdk'; +import dotenv from 'dotenv'; +import inquirer from 'inquirer'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.join(__dirname, '..'); @@ -99,20 +99,19 @@ async function checkRequiredEnvVars() { name: 'NEXT_PUBLIC_MINI_APP_NAME', message: 'Enter the name for your frame (e.g., My Cool Mini App):', default: process.env.NEXT_PUBLIC_MINI_APP_NAME, - validate: (input) => - input.trim() !== '' || 'Mini app name cannot be empty', + validate: input => input.trim() !== '' || 'Mini app name cannot be empty', }, { name: 'NEXT_PUBLIC_MINI_APP_BUTTON_TEXT', message: 'Enter the text for your frame button:', default: process.env.NEXT_PUBLIC_MINI_APP_BUTTON_TEXT ?? 'Launch Mini App', - validate: (input) => input.trim() !== '' || 'Button text cannot be empty', + validate: input => input.trim() !== '' || 'Button text cannot be empty', }, ]; const missingVars = requiredVars.filter( - (varConfig) => !process.env[varConfig.name] + varConfig => !process.env[varConfig.name], ); if (missingVars.length > 0) { @@ -138,7 +137,7 @@ async function checkRequiredEnvVars() { const newLine = envContent ? '\n' : ''; fs.appendFileSync( '.env', - `${newLine}${varConfig.name}="${value.trim()}"` + `${newLine}${varConfig.name}="${value.trim()}"`, ); } @@ -161,7 +160,7 @@ async function checkRequiredEnvVars() { if (storeSeedPhrase) { fs.appendFileSync( '.env.local', - `\nSPONSOR_SIGNER="${sponsorSigner}"` + `\nSPONSOR_SIGNER="${sponsorSigner}"`, ); console.log('✅ Sponsor signer preference stored in .env.local'); } @@ -244,7 +243,7 @@ async function getVercelToken() { return null; // We'll fall back to CLI operations } catch (error) { throw new Error( - 'Not logged in to Vercel CLI. Please run this script again to login.' + 'Not logged in to Vercel CLI. Please run this script again to login.', ); } } @@ -260,7 +259,7 @@ async function loginToVercel() { console.log('3. Complete the Vercel account setup in your browser'); console.log('4. Return here once your Vercel account is created\n'); console.log( - '\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account' + '\nNote: you may need to cancel this script with ctrl+c and run it again if creating a new vercel account', ); const child = spawn('vercel', ['login'], { @@ -268,14 +267,14 @@ async function loginToVercel() { }); await new Promise((resolve, reject) => { - child.on('close', (code) => { + child.on('close', code => { resolve(); }); }); console.log('\n📱 Waiting for login to complete...'); console.log( - "If you're creating a new account, please complete the Vercel account setup in your browser first." + "If you're creating a new account, please complete the Vercel account setup in your browser first.", ); for (let i = 0; i < 150; i++) { @@ -287,7 +286,7 @@ async function loginToVercel() { if (error.message.includes('Account not found')) { console.log('â„šī¸ Waiting for Vercel account setup to complete...'); } - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 2000)); } } @@ -313,7 +312,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { }); const existingVar = existingVars.envs?.find( - (env) => env.key === key && env.target?.includes('production') + env => env.key === key && env.target?.includes('production'), ); if (existingVar) { @@ -345,7 +344,7 @@ async function setVercelEnvVarSDK(vercelClient, projectId, key, value) { } catch (error) { console.warn( `âš ī¸ Warning: Failed to set environment variable ${key}:`, - error.message + error.message, ); return false; } @@ -400,7 +399,7 @@ async function setVercelEnvVarCLI(key, value, projectRoot) { } console.warn( `âš ī¸ Warning: Failed to set environment variable ${key}:`, - error.message + error.message, ); return false; } @@ -410,7 +409,7 @@ async function setEnvironmentVariables( vercelClient, projectId, envVars, - projectRoot + projectRoot, ) { console.log('\n📝 Setting up environment variables...'); @@ -435,12 +434,12 @@ async function setEnvironmentVariables( } // Report results - const failed = results.filter((r) => !r.success); + const failed = results.filter(r => !r.success); if (failed.length > 0) { console.warn(`\nâš ī¸ Failed to set ${failed.length} environment variables:`); - failed.forEach((r) => console.warn(` - ${r.key}`)); + failed.forEach(r => console.warn(` - ${r.key}`)); console.warn( - '\nYou may need to set these manually in the Vercel dashboard.' + '\nYou may need to set these manually in the Vercel dashboard.', ); } @@ -450,7 +449,7 @@ async function setEnvironmentVariables( async function waitForDeployment( vercelClient, projectId, - maxWaitTime = 300000 + maxWaitTime = 300000, ) { // 5 minutes console.log('\nâŗ Waiting for deployment to complete...'); @@ -477,14 +476,14 @@ async function waitForDeployment( } // Still building, wait and check again - await new Promise((resolve) => setTimeout(resolve, 5000)); // Wait 5 seconds + await new Promise(resolve => setTimeout(resolve, 5000)); // Wait 5 seconds } else { console.log('âŗ No deployment found yet, waiting...'); - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise(resolve => setTimeout(resolve, 5000)); } } catch (error) { console.warn('âš ī¸ Could not check deployment status:', error.message); - await new Promise((resolve) => setTimeout(resolve, 5000)); + await new Promise(resolve => setTimeout(resolve, 5000)); } } @@ -507,18 +506,18 @@ async function deployToVercel(useGitHub = false) { framework: 'nextjs', }, null, - 2 - ) + 2, + ), ); } // Set up Vercel project console.log('\nđŸ“Ļ Setting up Vercel project...'); console.log( - 'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n' + 'An initial deployment is required to get an assigned domain that can be used in the mini app manifest\n', ); console.log( - '\nâš ī¸ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n' + '\nâš ī¸ Note: choosing a longer, more unique project name will help avoid conflicts with other existing domains\n', ); // Use spawn instead of execSync for better error handling @@ -530,7 +529,7 @@ async function deployToVercel(useGitHub = false) { }); await new Promise((resolve, reject) => { - vercelSetup.on('close', (code) => { + vercelSetup.on('close', code => { if (code === 0 || code === null) { console.log('✅ Vercel project setup completed'); resolve(); @@ -540,25 +539,25 @@ async function deployToVercel(useGitHub = false) { } }); - vercelSetup.on('error', (error) => { + vercelSetup.on('error', error => { console.log('âš ī¸ Vercel setup command completed (this is normal)'); resolve(); // Don't reject, as this is often expected }); }); // Wait a moment for project files to be written - await new Promise((resolve) => setTimeout(resolve, 2000)); + await new Promise(resolve => setTimeout(resolve, 2000)); // Load project info let projectId; try { const projectJson = JSON.parse( - fs.readFileSync('.vercel/project.json', 'utf8') + fs.readFileSync('.vercel/project.json', 'utf8'), ); projectId = projectJson.projectId; } catch (error) { throw new Error( - 'Failed to load project info. Please ensure the Vercel project was created successfully.' + 'Failed to load project info. Please ensure the Vercel project was created successfully.', ); } @@ -574,7 +573,7 @@ async function deployToVercel(useGitHub = false) { } } catch (error) { console.warn( - 'âš ī¸ Could not initialize Vercel SDK, falling back to CLI operations' + 'âš ī¸ Could not initialize Vercel SDK, falling back to CLI operations', ); } @@ -593,7 +592,7 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Using project name for domain:', domain); } catch (error) { console.warn( - 'âš ī¸ Could not get project details via SDK, using CLI fallback' + 'âš ī¸ Could not get project details via SDK, using CLI fallback', ); } } @@ -606,7 +605,7 @@ async function deployToVercel(useGitHub = false) { { cwd: projectRoot, encoding: 'utf8', - } + }, ); const nameMatch = inspectOutput.match(/Name\s+([^\n]+)/); @@ -622,7 +621,7 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Using project name for domain:', domain); } else { console.warn( - 'âš ī¸ Could not determine project name from inspection, using fallback' + 'âš ī¸ Could not determine project name from inspection, using fallback', ); // Use a fallback domain based on project ID domain = `project-${projectId.slice(-8)}.vercel.app`; @@ -670,8 +669,8 @@ async function deployToVercel(useGitHub = false) { ...Object.fromEntries( Object.entries(process.env).filter(([key]) => - key.startsWith('NEXT_PUBLIC_') - ) + key.startsWith('NEXT_PUBLIC_'), + ), ), }; @@ -680,7 +679,7 @@ async function deployToVercel(useGitHub = false) { vercelClient, projectId, vercelEnv, - projectRoot + projectRoot, ); // Deploy the project @@ -704,7 +703,7 @@ async function deployToVercel(useGitHub = false) { }); await new Promise((resolve, reject) => { - vercelDeploy.on('close', (code) => { + vercelDeploy.on('close', code => { if (code === 0) { console.log('✅ Vercel deployment command completed'); resolve(); @@ -714,7 +713,7 @@ async function deployToVercel(useGitHub = false) { } }); - vercelDeploy.on('error', (error) => { + vercelDeploy.on('error', error => { console.error('❌ Vercel deployment error:', error.message); reject(error); }); @@ -728,7 +727,7 @@ async function deployToVercel(useGitHub = false) { } catch (error) { console.warn( 'âš ī¸ Could not verify deployment completion:', - error.message + error.message, ); console.log('â„šī¸ Proceeding with domain verification...'); } @@ -744,7 +743,7 @@ async function deployToVercel(useGitHub = false) { console.log('🌐 Verified actual domain:', actualDomain); } catch (error) { console.warn( - 'âš ī¸ Could not verify domain via SDK, using assumed domain' + 'âš ī¸ Could not verify domain via SDK, using assumed domain', ); } } @@ -769,7 +768,7 @@ async function deployToVercel(useGitHub = false) { fid, await validateSeedPhrase(process.env.SEED_PHRASE), process.env.SEED_PHRASE, - webhookUrl + webhookUrl, ); updatedEnv.MINI_APP_METADATA = updatedMetadata; } @@ -778,7 +777,7 @@ async function deployToVercel(useGitHub = false) { vercelClient, projectId, updatedEnv, - projectRoot + projectRoot, ); console.log('\nđŸ“Ļ Redeploying with correct domain...'); @@ -789,7 +788,7 @@ async function deployToVercel(useGitHub = false) { }); await new Promise((resolve, reject) => { - vercelRedeploy.on('close', (code) => { + vercelRedeploy.on('close', code => { if (code === 0) { console.log('✅ Redeployment completed'); resolve(); @@ -799,7 +798,7 @@ async function deployToVercel(useGitHub = false) { } }); - vercelRedeploy.on('error', (error) => { + vercelRedeploy.on('error', error => { console.error('❌ Redeployment error:', error.message); reject(error); }); @@ -811,7 +810,7 @@ async function deployToVercel(useGitHub = false) { console.log('\n✨ Deployment complete! Your mini app is now live at:'); console.log(`🌐 https://${domain}`); console.log( - '\n📝 You can manage your project at https://vercel.com/dashboard' + '\n📝 You can manage your project at https://vercel.com/dashboard', ); } catch (error) { console.error('\n❌ Deployment failed:', error.message); @@ -823,7 +822,7 @@ async function main() { try { console.log('🚀 Vercel Mini App Deployment (SDK Edition)'); console.log( - 'This script will deploy your mini app to Vercel using the Vercel SDK.' + 'This script will deploy your mini app to Vercel using the Vercel SDK.', ); console.log('\nThe script will:'); console.log('1. Check for required environment variables'); @@ -902,4 +901,4 @@ async function main() { } } -main(); \ No newline at end of file +main(); diff --git a/src/app/api/auth/nonce/route.ts b/src/app/api/auth/nonce/route.ts index a1f25ea..8d5c51d 100644 --- a/src/app/api/auth/nonce/route.ts +++ b/src/app/api/auth/nonce/route.ts @@ -10,7 +10,7 @@ export async function GET() { console.error('Error fetching nonce:', error); return NextResponse.json( { error: 'Failed to fetch nonce' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/auth/session-signers/route.ts b/src/app/api/auth/session-signers/route.ts index 630ef3b..d5b8a6e 100644 --- a/src/app/api/auth/session-signers/route.ts +++ b/src/app/api/auth/session-signers/route.ts @@ -10,7 +10,7 @@ export async function GET(request: Request) { if (!message || !signature) { return NextResponse.json( { error: 'Message and signature are required' }, - { status: 400 } + { status: 400 }, ); } @@ -37,7 +37,7 @@ export async function GET(request: Request) { console.error('Error in session-signers API:', error); return NextResponse.json( { error: 'Failed to fetch signers' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/auth/signer/route.ts b/src/app/api/auth/signer/route.ts index f793d0e..d80f428 100644 --- a/src/app/api/auth/signer/route.ts +++ b/src/app/api/auth/signer/route.ts @@ -10,7 +10,7 @@ export async function POST() { console.error('Error fetching signer:', error); return NextResponse.json( { error: 'Failed to fetch signer' }, - { status: 500 } + { status: 500 }, ); } } @@ -22,7 +22,7 @@ export async function GET(request: Request) { if (!signerUuid) { return NextResponse.json( { error: 'signerUuid is required' }, - { status: 400 } + { status: 400 }, ); } @@ -36,7 +36,7 @@ export async function GET(request: Request) { console.error('Error fetching signed key:', error); return NextResponse.json( { error: 'Failed to fetch signed key' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/auth/signer/signed_key/route.ts b/src/app/api/auth/signer/signed_key/route.ts index d7a3df8..a78dc34 100644 --- a/src/app/api/auth/signer/signed_key/route.ts +++ b/src/app/api/auth/signer/signed_key/route.ts @@ -1,10 +1,10 @@ import { NextResponse } from 'next/server'; -import { getNeynarClient } from '~/lib/neynar'; import { mnemonicToAccount } from 'viem/accounts'; import { SIGNED_KEY_REQUEST_TYPE, SIGNED_KEY_REQUEST_VALIDATOR_EIP_712_DOMAIN, } from '~/lib/constants'; +import { getNeynarClient } from '~/lib/neynar'; const postRequiredFields = ['signerUuid', 'publicKey']; @@ -16,7 +16,7 @@ export async function POST(request: Request) { if (!body[field]) { return NextResponse.json( { error: `${field} is required` }, - { status: 400 } + { status: 400 }, ); } } @@ -26,7 +26,7 @@ export async function POST(request: Request) { if (redirectUrl && typeof redirectUrl !== 'string') { return NextResponse.json( { error: 'redirectUrl must be a string' }, - { status: 400 } + { status: 400 }, ); } @@ -38,7 +38,7 @@ export async function POST(request: Request) { if (!seedPhrase) { return NextResponse.json( { error: 'App configuration missing (SEED_PHRASE or FID)' }, - { status: 500 } + { status: 500 }, ); } @@ -85,7 +85,7 @@ export async function POST(request: Request) { console.error('Error registering signed key:', error); return NextResponse.json( { error: 'Failed to register signed key' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/auth/signers/route.ts b/src/app/api/auth/signers/route.ts index 1c89acf..18cc5d7 100644 --- a/src/app/api/auth/signers/route.ts +++ b/src/app/api/auth/signers/route.ts @@ -13,7 +13,7 @@ export async function GET(request: Request) { { error: `${param} parameter is required`, }, - { status: 400 } + { status: 400 }, ); } } @@ -32,7 +32,7 @@ export async function GET(request: Request) { console.error('Error fetching signers:', error); return NextResponse.json( { error: 'Failed to fetch signers' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/auth/update-session/route.ts b/src/app/api/auth/update-session/route.ts index db4b4fc..34491d5 100644 --- a/src/app/api/auth/update-session/route.ts +++ b/src/app/api/auth/update-session/route.ts @@ -9,7 +9,7 @@ export async function POST(request: Request) { if (!session?.user?.fid) { return NextResponse.json( { error: 'No authenticated session found' }, - { status: 401 } + { status: 401 }, ); } @@ -19,7 +19,7 @@ export async function POST(request: Request) { if (!signers || !user) { return NextResponse.json( { error: 'Signers and user are required' }, - { status: 400 } + { status: 400 }, ); } @@ -40,7 +40,7 @@ export async function POST(request: Request) { console.error('Error preparing session update:', error); return NextResponse.json( { error: 'Failed to prepare session update' }, - { status: 500 } + { status: 500 }, ); } } diff --git a/src/app/api/send-notification/route.ts b/src/app/api/send-notification/route.ts index 8f8c301..92bb705 100644 --- a/src/app/api/send-notification/route.ts +++ b/src/app/api/send-notification/route.ts @@ -1,9 +1,9 @@ -import { notificationDetailsSchema } from "@farcaster/miniapp-sdk"; -import { NextRequest } from "next/server"; -import { z } from "zod"; -import { setUserNotificationDetails } from "~/lib/kv"; -import { sendMiniAppNotification } from "~/lib/notifs"; -import { sendNeynarMiniAppNotification } from "~/lib/neynar"; +import { NextRequest } from 'next/server'; +import { notificationDetailsSchema } from '@farcaster/miniapp-sdk'; +import { z } from 'zod'; +import { setUserNotificationDetails } from '~/lib/kv'; +import { sendNeynarMiniAppNotification } from '~/lib/neynar'; +import { sendMiniAppNotification } from '~/lib/notifs'; const requestSchema = z.object({ fid: z.number(), @@ -13,7 +13,8 @@ const requestSchema = z.object({ export async function POST(request: NextRequest) { // If Neynar is enabled, we don't need to store notification details // as they will be managed by Neynar's system - const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID; + const neynarEnabled = + process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID; const requestJson = await request.json(); const requestBody = requestSchema.safeParse(requestJson); @@ -21,7 +22,7 @@ export async function POST(request: NextRequest) { if (requestBody.success === false) { return Response.json( { success: false, errors: requestBody.error.errors }, - { status: 400 } + { status: 400 }, ); } @@ -29,29 +30,31 @@ export async function POST(request: NextRequest) { if (!neynarEnabled) { await setUserNotificationDetails( Number(requestBody.data.fid), - requestBody.data.notificationDetails + requestBody.data.notificationDetails, ); } // Use appropriate notification function based on Neynar status - const sendNotification = neynarEnabled ? sendNeynarMiniAppNotification : sendMiniAppNotification; + const sendNotification = neynarEnabled + ? sendNeynarMiniAppNotification + : sendMiniAppNotification; const sendResult = await sendNotification({ fid: Number(requestBody.data.fid), - title: "Test notification", - body: "Sent at " + new Date().toISOString(), + title: 'Test notification', + body: 'Sent at ' + new Date().toISOString(), }); - if (sendResult.state === "error") { + if (sendResult.state === 'error') { return Response.json( { success: false, error: sendResult.error }, - { status: 500 } + { status: 500 }, ); - } else if (sendResult.state === "rate_limit") { + } else if (sendResult.state === 'rate_limit') { return Response.json( - { success: false, error: "Rate limited" }, - { status: 429 } + { success: false, error: 'Rate limited' }, + { status: 429 }, ); } return Response.json({ success: true }); -} \ No newline at end of file +} diff --git a/src/app/api/webhook/route.ts b/src/app/api/webhook/route.ts index aec184e..5aad81e 100644 --- a/src/app/api/webhook/route.ts +++ b/src/app/api/webhook/route.ts @@ -1,20 +1,21 @@ +import { NextRequest } from 'next/server'; import { ParseWebhookEvent, parseWebhookEvent, verifyAppKeyWithNeynar, -} from "@farcaster/miniapp-node"; -import { NextRequest } from "next/server"; -import { APP_NAME } from "~/lib/constants"; +} from '@farcaster/miniapp-node'; +import { APP_NAME } from '~/lib/constants'; import { deleteUserNotificationDetails, setUserNotificationDetails, -} from "~/lib/kv"; -import { sendMiniAppNotification } from "~/lib/notifs"; +} from '~/lib/kv'; +import { sendMiniAppNotification } from '~/lib/notifs'; export async function POST(request: NextRequest) { // If Neynar is enabled, we don't need to handle webhooks here // as they will be handled by Neynar's webhook endpoint - const neynarEnabled = process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID; + const neynarEnabled = + process.env.NEYNAR_API_KEY && process.env.NEYNAR_CLIENT_ID; if (neynarEnabled) { return Response.json({ success: true }); } @@ -28,24 +29,24 @@ export async function POST(request: NextRequest) { const error = e as ParseWebhookEvent.ErrorType; switch (error.name) { - case "VerifyJsonFarcasterSignature.InvalidDataError": - case "VerifyJsonFarcasterSignature.InvalidEventDataError": + case 'VerifyJsonFarcasterSignature.InvalidDataError': + case 'VerifyJsonFarcasterSignature.InvalidEventDataError': // The request data is invalid return Response.json( { success: false, error: error.message }, - { status: 400 } + { status: 400 }, ); - case "VerifyJsonFarcasterSignature.InvalidAppKeyError": + case 'VerifyJsonFarcasterSignature.InvalidAppKeyError': // The app key is invalid return Response.json( { success: false, error: error.message }, - { status: 401 } + { status: 401 }, ); - case "VerifyJsonFarcasterSignature.VerifyAppKeyError": + case 'VerifyJsonFarcasterSignature.VerifyAppKeyError': // Internal error verifying the app key (caller may want to try again) return Response.json( { success: false, error: error.message }, - { status: 500 } + { status: 500 }, ); } } @@ -56,36 +57,36 @@ export async function POST(request: NextRequest) { // Only handle notifications if Neynar is not enabled // When Neynar is enabled, notifications are handled through their webhook switch (event.event) { - case "frame_added": + case 'frame_added': if (event.notificationDetails) { await setUserNotificationDetails(fid, event.notificationDetails); await sendMiniAppNotification({ fid, title: `Welcome to ${APP_NAME}`, - body: "Mini app is now added to your client", + body: 'Mini app is now added to your client', }); } else { await deleteUserNotificationDetails(fid); } break; - case "frame_removed": + case 'frame_removed': await deleteUserNotificationDetails(fid); break; - case "notifications_enabled": + case 'notifications_enabled': await setUserNotificationDetails(fid, event.notificationDetails); await sendMiniAppNotification({ fid, title: `Welcome to ${APP_NAME}`, - body: "Notifications are now enabled", + body: 'Notifications are now enabled', }); break; - case "notifications_disabled": + case 'notifications_disabled': await deleteUserNotificationDetails(fid); break; } return Response.json({ success: true }); -} \ No newline at end of file +} diff --git a/src/app/providers.tsx b/src/app/providers.tsx index 014110a..5970d14 100644 --- a/src/app/providers.tsx +++ b/src/app/providers.tsx @@ -1,18 +1,18 @@ 'use client'; import dynamic from 'next/dynamic'; +import { AuthKitProvider } from '@farcaster/auth-kit'; +import { MiniAppProvider } from '@neynar/react'; import type { Session } from 'next-auth'; import { SessionProvider } from 'next-auth/react'; -import { MiniAppProvider } from '@neynar/react'; import { SafeFarcasterSolanaProvider } from '~/components/providers/SafeFarcasterSolanaProvider'; import { ANALYTICS_ENABLED } from '~/lib/constants'; -import { AuthKitProvider } from '@farcaster/auth-kit'; const WagmiProvider = dynamic( () => import('~/components/providers/WagmiProvider'), { ssr: false, - } + }, ); export function Providers({ @@ -38,4 +38,4 @@ export function Providers({ ); -} \ No newline at end of file +} diff --git a/src/auth.ts b/src/auth.ts index d77fbba..a790041 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,6 +1,6 @@ +import { createAppClient, viemConnector } from '@farcaster/auth-client'; import { AuthOptions, getServerSession } from 'next-auth'; import CredentialsProvider from 'next-auth/providers/credentials'; -import { createAppClient, viemConnector } from '@farcaster/auth-client'; declare module 'next-auth' { interface Session { @@ -401,7 +401,7 @@ export const authOptions: AuthOptions = { }, cookies: { sessionToken: { - name: `next-auth.session-token`, + name: 'next-auth.session-token', options: { httpOnly: true, sameSite: 'none', @@ -410,7 +410,7 @@ export const authOptions: AuthOptions = { }, }, callbackUrl: { - name: `next-auth.callback-url`, + name: 'next-auth.callback-url', options: { sameSite: 'none', path: '/', @@ -418,7 +418,7 @@ export const authOptions: AuthOptions = { }, }, csrfToken: { - name: `next-auth.csrf-token`, + name: 'next-auth.csrf-token', options: { httpOnly: true, sameSite: 'none', @@ -436,4 +436,4 @@ export const getSession = async () => { console.error('Error getting server session:', error); return null; } -}; \ No newline at end of file +}; diff --git a/src/components/providers/SafeFarcasterSolanaProvider.tsx b/src/components/providers/SafeFarcasterSolanaProvider.tsx index ee6d923..cc29d64 100644 --- a/src/components/providers/SafeFarcasterSolanaProvider.tsx +++ b/src/components/providers/SafeFarcasterSolanaProvider.tsx @@ -1,10 +1,13 @@ -import React, { createContext, useEffect, useState } from "react"; -import dynamic from "next/dynamic"; +import React, { createContext, useEffect, useState } from 'react'; +import dynamic from 'next/dynamic'; import { sdk } from '@farcaster/miniapp-sdk'; const FarcasterSolanaProvider = dynamic( - () => import('@farcaster/mini-app-solana').then(mod => mod.FarcasterSolanaProvider), - { ssr: false } + () => + import('@farcaster/mini-app-solana').then( + mod => mod.FarcasterSolanaProvider, + ), + { ssr: false }, ); type SafeFarcasterSolanaProviderProps = { @@ -12,10 +15,15 @@ type SafeFarcasterSolanaProviderProps = { children: React.ReactNode; }; -const SolanaProviderContext = createContext<{ hasSolanaProvider: boolean }>({ hasSolanaProvider: false }); +const SolanaProviderContext = createContext<{ hasSolanaProvider: boolean }>({ + hasSolanaProvider: false, +}); -export function SafeFarcasterSolanaProvider({ endpoint, children }: SafeFarcasterSolanaProviderProps) { - const isClient = typeof window !== "undefined"; +export function SafeFarcasterSolanaProvider({ + endpoint, + children, +}: SafeFarcasterSolanaProviderProps) { + const isClient = typeof window !== 'undefined'; const [hasSolanaProvider, setHasSolanaProvider] = useState(false); const [checked, setChecked] = useState(false); @@ -48,8 +56,8 @@ export function SafeFarcasterSolanaProvider({ endpoint, children }: SafeFarcaste const origError = console.error; console.error = (...args) => { if ( - typeof args[0] === "string" && - args[0].includes("WalletConnectionError: could not get Solana provider") + typeof args[0] === 'string' && + args[0].includes('WalletConnectionError: could not get Solana provider') ) { if (!errorShown) { origError(...args); @@ -83,4 +91,4 @@ export function SafeFarcasterSolanaProvider({ endpoint, children }: SafeFarcaste export function useHasSolanaProvider() { return React.useContext(SolanaProviderContext).hasSolanaProvider; -} \ No newline at end of file +} diff --git a/src/components/providers/WagmiProvider.tsx b/src/components/providers/WagmiProvider.tsx index 7f5e9c8..21b724b 100644 --- a/src/components/providers/WagmiProvider.tsx +++ b/src/components/providers/WagmiProvider.tsx @@ -1,12 +1,12 @@ -import { createConfig, http, WagmiProvider } from "wagmi"; -import { base, degen, mainnet, optimism, unichain, celo } from "wagmi/chains"; -import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { farcasterFrame } from "@farcaster/miniapp-wagmi-connector"; +import React from 'react'; +import { useEffect, useState } from 'react'; +import { farcasterFrame } from '@farcaster/miniapp-wagmi-connector'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createConfig, http, WagmiProvider } from 'wagmi'; +import { useConnect, useAccount } from 'wagmi'; +import { base, degen, mainnet, optimism, unichain, celo } from 'wagmi/chains'; import { coinbaseWallet, metaMask } from 'wagmi/connectors'; -import { APP_NAME, APP_ICON_URL, APP_URL } from "~/lib/constants"; -import { useEffect, useState } from "react"; -import { useConnect, useAccount } from "wagmi"; -import React from "react"; +import { APP_NAME, APP_ICON_URL, APP_URL } from '~/lib/constants'; // Custom hook for Coinbase Wallet detection and auto-connection function useCoinbaseWalletAutoConnect() { @@ -17,15 +17,16 @@ function useCoinbaseWalletAutoConnect() { useEffect(() => { // Check if we're running in Coinbase Wallet const checkCoinbaseWallet = () => { - const isInCoinbaseWallet = window.ethereum?.isCoinbaseWallet || + const isInCoinbaseWallet = + window.ethereum?.isCoinbaseWallet || window.ethereum?.isCoinbaseWalletExtension || window.ethereum?.isCoinbaseWalletBrowser; setIsCoinbaseWallet(!!isInCoinbaseWallet); }; - + checkCoinbaseWallet(); window.addEventListener('ethereum#initialized', checkCoinbaseWallet); - + return () => { window.removeEventListener('ethereum#initialized', checkCoinbaseWallet); }; @@ -70,7 +71,11 @@ export const config = createConfig({ const queryClient = new QueryClient(); // Wrapper component that provides Coinbase Wallet auto-connection -function CoinbaseWalletAutoConnect({ children }: { children: React.ReactNode }) { +function CoinbaseWalletAutoConnect({ + children, +}: { + children: React.ReactNode; +}) { useCoinbaseWalletAutoConnect(); return <>{children}; } @@ -79,10 +84,8 @@ export default function Provider({ children }: { children: React.ReactNode }) { return ( - - {children} - + {children} ); -} \ No newline at end of file +} diff --git a/src/components/ui/Header.tsx b/src/components/ui/Header.tsx index eee6c22..8646624 100644 --- a/src/components/ui/Header.tsx +++ b/src/components/ui/Header.tsx @@ -1,9 +1,9 @@ -"use client"; +'use client'; -import { useState } from "react"; -import { APP_NAME } from "~/lib/constants"; -import sdk from "@farcaster/miniapp-sdk"; -import { useMiniApp } from "@neynar/react"; +import { useState } from 'react'; +import sdk from '@farcaster/miniapp-sdk'; +import { useMiniApp } from '@neynar/react'; +import { APP_NAME } from '~/lib/constants'; type HeaderProps = { neynarUser?: { @@ -18,23 +18,19 @@ export function Header({ neynarUser }: HeaderProps) { return (
-
-
- Welcome to {APP_NAME}! -
+
+
Welcome to {APP_NAME}!
{context?.user && ( -
{ setIsUserDropdownOpen(!isUserDropdownOpen); }} > {context.user.pfpUrl && ( - Profile )} @@ -42,14 +38,16 @@ export function Header({ neynarUser }: HeaderProps) { )}
{context?.user && ( - <> + <> {isUserDropdownOpen && (
-

sdk.actions.viewProfile({ fid: context.user.fid })} + onClick={() => + sdk.actions.viewProfile({ fid: context.user.fid }) + } > {context.user.displayName || context.user.username}

@@ -74,4 +72,4 @@ export function Header({ neynarUser }: HeaderProps) { )}
); -} \ No newline at end of file +} diff --git a/src/components/ui/NeynarAuthButton/AuthDialog.tsx b/src/components/ui/NeynarAuthButton/AuthDialog.tsx index a458ab4..5a11a50 100644 --- a/src/components/ui/NeynarAuthButton/AuthDialog.tsx +++ b/src/components/ui/NeynarAuthButton/AuthDialog.tsx @@ -169,7 +169,7 @@ export function AuthDialog({ {/* eslint-disable-next-line @next/next/no-img-element */} QR Code {/* eslint-disable-next-line @next/next/no-img-element */} @@ -35,7 +35,7 @@ export function ProfileButton({ src={pfpUrl} alt="Profile" className="w-6 h-6 rounded-full object-cover flex-shrink-0" - onError={(e) => { + onError={e => { (e.target as HTMLImageElement).src = 'https://farcaster.xyz/avatar.png'; }} @@ -46,7 +46,7 @@ export function ProfileButton({ ( - 'loading' + 'loading', ); const [signerApprovalUrl, setSignerApprovalUrl] = useState( - null + null, ); const [pollingInterval, setPollingInterval] = useState( - null + null, ); const [message, setMessage] = useState(null); const [signature, setSignature] = useState(null); @@ -141,7 +141,7 @@ export function NeynarAuthButton() { const updateSessionWithSigners = useCallback( async ( signers: StoredAuthState['signers'], - user: StoredAuthState['user'] + user: StoredAuthState['user'], ) => { if (!useBackendFlow) return; @@ -164,7 +164,7 @@ export function NeynarAuthButton() { console.error('❌ Error updating session with signers:', error); } }, - [useBackendFlow, message, signature, nonce] + [useBackendFlow, message, signature, nonce], ); // Helper function to fetch user data from Neynar API @@ -182,7 +182,7 @@ export function NeynarAuthButton() { return null; } }, - [] + [], ); // Helper function to generate signed key request @@ -210,7 +210,7 @@ export function NeynarAuthButton() { if (!response.ok) { const errorData = await response.json(); throw new Error( - `Failed to generate signed key request: ${errorData.error}` + `Failed to generate signed key request: ${errorData.error}`, ); } @@ -222,7 +222,7 @@ export function NeynarAuthButton() { // throw error; } }, - [] + [], ); // Helper function to fetch all signers @@ -233,10 +233,10 @@ export function NeynarAuthButton() { const endpoint = useBackendFlow ? `/api/auth/session-signers?message=${encodeURIComponent( - message + message, )}&signature=${signature}` : `/api/auth/signers?message=${encodeURIComponent( - message + message, )}&signature=${signature}`; const response = await fetch(endpoint); @@ -258,7 +258,7 @@ export function NeynarAuthButton() { if (signerData.signers && signerData.signers.length > 0) { const fetchedUser = (await fetchUserData( - signerData.signers[0].fid + signerData.signers[0].fid, )) as StoredAuthState['user']; user = fetchedUser; } @@ -285,7 +285,7 @@ export function NeynarAuthButton() { setSignersLoading(false); } }, - [useBackendFlow, fetchUserData, updateSessionWithSigners] + [useBackendFlow, fetchUserData, updateSessionWithSigners], ); // Helper function to poll signer status @@ -308,10 +308,10 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + try { const response = await fetch( - `/api/auth/signer?signerUuid=${signerUuid}` + `/api/auth/signer?signerUuid=${signerUuid}`, ); if (!response.ok) { @@ -321,7 +321,7 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + // Increment retry count for other errors retryCount++; if (retryCount >= maxRetries) { @@ -329,7 +329,7 @@ export function NeynarAuthButton() { setPollingInterval(null); return; } - + throw new Error(`Failed to poll signer status: ${response.status}`); } @@ -352,7 +352,7 @@ export function NeynarAuthButton() { setPollingInterval(interval); }, - [fetchAllSigners, pollingInterval] + [fetchAllSigners, pollingInterval], ); // Cleanup polling on unmount @@ -412,7 +412,7 @@ export function NeynarAuthButton() { } // For backend flow, the session will be handled by NextAuth }, - [useBackendFlow, fetchUserData] + [useBackendFlow, fetchUserData], ); // Error callback @@ -443,7 +443,7 @@ export function NeynarAuthButton() { useEffect(() => { setMessage(data?.message || null); setSignature(data?.signature || null); - + // Reset the signer flow flag when message/signature change if (data?.message && data?.signature) { signerFlowStartedRef.current = false; @@ -459,9 +459,14 @@ export function NeynarAuthButton() { // Handle fetching signers after successful authentication useEffect(() => { - if (message && signature && !isSignerFlowRunning && !signerFlowStartedRef.current) { + if ( + message && + signature && + !isSignerFlowRunning && + !signerFlowStartedRef.current + ) { signerFlowStartedRef.current = true; - + const handleSignerFlow = async () => { setIsSignerFlowRunning(true); try { @@ -479,7 +484,7 @@ export function NeynarAuthButton() { // First, fetch existing signers const signers = await fetchAllSigners(message, signature); - + if (useBackendFlow && isMobileContext) setSignersLoading(true); // Check if no signers exist or if we have empty signers @@ -490,7 +495,7 @@ export function NeynarAuthButton() { // Step 2: Generate signed key request const signedKeyData = await generateSignedKeyRequest( newSigner.signer_uuid, - newSigner.public_key + newSigner.public_key, ); // Step 3: Show QR code in access dialog for signer approval @@ -501,8 +506,8 @@ export function NeynarAuthButton() { await sdk.actions.openUrl( signedKeyData.signer_approval_url.replace( 'https://client.farcaster.xyz/deeplinks/signed-key-request', - 'https://farcaster.xyz/~/connect' - ) + 'https://farcaster.xyz/~/connect', + ), ); } else { setShowDialog(true); // Ensure dialog is shown during loading @@ -604,7 +609,7 @@ export function NeynarAuthButton() { clearInterval(pollingInterval); setPollingInterval(null); } - + // Reset signer flow flag signerFlowStartedRef.current = false; } catch (error) { @@ -663,7 +668,7 @@ export function NeynarAuthButton() { 'btn btn-primary flex items-center gap-3', 'disabled:opacity-50 disabled:cursor-not-allowed', 'transform transition-all duration-200 active:scale-[0.98]', - !url && !useBackendFlow && 'cursor-not-allowed' + !url && !useBackendFlow && 'cursor-not-allowed', )} > {!useBackendFlow && !url ? ( diff --git a/src/components/ui/Share.tsx b/src/components/ui/Share.tsx index 526a255..d44a442 100644 --- a/src/components/ui/Share.tsx +++ b/src/components/ui/Share.tsx @@ -1,9 +1,9 @@ 'use client'; import { useCallback, useState, useEffect } from 'react'; -import { Button } from './Button'; +import { type ComposeCast } from '@farcaster/miniapp-sdk'; import { useMiniApp } from '@neynar/react'; -import { type ComposeCast } from "@farcaster/miniapp-sdk"; +import { Button } from './Button'; interface EmbedConfig { path?: string; @@ -23,9 +23,16 @@ interface ShareButtonProps { isLoading?: boolean; } -export function ShareButton({ buttonText, cast, className = '', isLoading = false }: ShareButtonProps) { +export function ShareButton({ + buttonText, + cast, + className = '', + isLoading = false, +}: ShareButtonProps) { const [isProcessing, setIsProcessing] = useState(false); - const [bestFriends, setBestFriends] = useState<{ fid: number; username: string; }[] | null>(null); + const [bestFriends, setBestFriends] = useState< + { fid: number; username: string }[] | null + >(null); const [isLoadingBestFriends, setIsLoadingBestFriends] = useState(false); const { context, actions } = useMiniApp(); @@ -51,7 +58,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals if (cast.bestFriends) { if (bestFriends) { // Replace @N with usernames, or remove if no matching friend - finalText = finalText.replace(/@\d+/g, (match) => { + finalText = finalText.replace(/@\d+/g, match => { const friendIndex = parseInt(match.slice(1)) - 1; const friend = bestFriends[friendIndex]; if (friend) { @@ -67,16 +74,20 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals // Process embeds const processedEmbeds = await Promise.all( - (cast.embeds || []).map(async (embed) => { + (cast.embeds || []).map(async embed => { if (typeof embed === 'string') { return embed; } if (embed.path) { - const baseUrl = process.env.NEXT_PUBLIC_URL || window.location.origin; + const baseUrl = + process.env.NEXT_PUBLIC_URL || window.location.origin; const url = new URL(`${baseUrl}${embed.path}`); // Add UTM parameters - url.searchParams.set('utm_source', `share-cast-${context?.user?.fid || 'unknown'}`); + url.searchParams.set( + 'utm_source', + `share-cast-${context?.user?.fid || 'unknown'}`, + ); // If custom image generator is provided, use it if (embed.imageUrl) { @@ -87,7 +98,7 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals return url.toString(); } return embed.url || ''; - }) + }), ); // Open cast composer with all supported intents @@ -115,4 +126,4 @@ export function ShareButton({ buttonText, cast, className = '', isLoading = fals {buttonText} ); -} \ No newline at end of file +} diff --git a/src/components/ui/tabs/ActionsTab.tsx b/src/components/ui/tabs/ActionsTab.tsx index 44cf94d..16abeb8 100644 --- a/src/components/ui/tabs/ActionsTab.tsx +++ b/src/components/ui/tabs/ActionsTab.tsx @@ -1,12 +1,12 @@ 'use client'; -import { useCallback, useState } from "react"; -import { useMiniApp } from "@neynar/react"; -import { ShareButton } from "../Share"; -import { Button } from "../Button"; -import { SignIn } from "../wallet/SignIn"; -import { type Haptics } from "@farcaster/miniapp-sdk"; +import { useCallback, useState } from 'react'; +import { type Haptics } from '@farcaster/miniapp-sdk'; +import { useMiniApp } from '@neynar/react'; +import { Button } from '../Button'; import { NeynarAuthButton } from '../NeynarAuthButton/index'; +import { ShareButton } from '../Share'; +import { SignIn } from '../wallet/SignIn'; /** * ActionsTab component handles mini app actions like sharing, notifications, and haptic feedback. @@ -51,7 +51,7 @@ export function ActionsTab() { * @returns Promise that resolves when the notification is sent or fails */ const sendFarcasterNotification = useCallback(async () => { - setNotificationState((prev) => ({ ...prev, sendStatus: '' })); + setNotificationState(prev => ({ ...prev, sendStatus: '' })); if (!notificationDetails || !context) { return; } @@ -66,22 +66,22 @@ export function ActionsTab() { }), }); if (response.status === 200) { - setNotificationState((prev) => ({ ...prev, sendStatus: 'Success' })); + setNotificationState(prev => ({ ...prev, sendStatus: 'Success' })); return; } else if (response.status === 429) { - setNotificationState((prev) => ({ + setNotificationState(prev => ({ ...prev, sendStatus: 'Rate limited', })); return; } const responseText = await response.text(); - setNotificationState((prev) => ({ + setNotificationState(prev => ({ ...prev, sendStatus: `Error: ${responseText}`, })); } catch (error) { - setNotificationState((prev) => ({ + setNotificationState(prev => ({ ...prev, sendStatus: `Error: ${error}`, })); @@ -98,11 +98,11 @@ export function ActionsTab() { if (context?.user?.fid) { const userShareUrl = `${process.env.NEXT_PUBLIC_URL}/share/${context.user.fid}`; await navigator.clipboard.writeText(userShareUrl); - setNotificationState((prev) => ({ ...prev, shareUrlCopied: true })); + setNotificationState(prev => ({ ...prev, shareUrlCopied: true })); setTimeout( () => - setNotificationState((prev) => ({ ...prev, shareUrlCopied: false })), - 2000 + setNotificationState(prev => ({ ...prev, shareUrlCopied: false })), + 2000, ); } }, [context?.user?.fid]); @@ -123,10 +123,10 @@ export function ActionsTab() { // --- Render --- return ( -
+
{/* Share functionality */} {/* Authentication */} @@ -148,25 +148,25 @@ export function ActionsTab() { onClick={() => actions.openUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ') } - className='w-full' + className="w-full" > Open Link - {/* Notification functionality */} {notificationState.sendStatus && ( -
+
Send notification result: {notificationState.sendStatus}
)} @@ -175,24 +175,24 @@ export function ActionsTab() { {/* Haptic feedback controls */} -
-
); -} \ No newline at end of file +} diff --git a/src/components/ui/tabs/HomeTab.tsx b/src/components/ui/tabs/HomeTab.tsx index b1d0c94..058465d 100644 --- a/src/components/ui/tabs/HomeTab.tsx +++ b/src/components/ui/tabs/HomeTab.tsx @@ -17,7 +17,9 @@ export function HomeTab() {

Put your content here!

-

Powered by Neynar đŸĒ

+

+ Powered by Neynar đŸĒ +

); diff --git a/src/components/ui/wallet/SignIn.tsx b/src/components/ui/wallet/SignIn.tsx index 46d3c33..9dbd4fe 100644 --- a/src/components/ui/wallet/SignIn.tsx +++ b/src/components/ui/wallet/SignIn.tsx @@ -1,10 +1,10 @@ 'use client'; -import { useCallback, useState } from "react"; -import { signIn, signOut, getCsrfToken } from "next-auth/react"; -import sdk, { SignIn as SignInCore } from "@farcaster/miniapp-sdk"; -import { useSession } from "next-auth/react"; -import { Button } from "../Button"; +import { useCallback, useState } from 'react'; +import sdk, { SignIn as SignInCore } from '@farcaster/miniapp-sdk'; +import { signIn, signOut, getCsrfToken } from 'next-auth/react'; +import { useSession } from 'next-auth/react'; +import { Button } from '../Button'; /** * SignIn component handles Farcaster authentication using Sign-In with Farcaster (SIWF). @@ -72,7 +72,7 @@ export function SignIn() { */ const handleSignIn = useCallback(async () => { try { - setAuthState((prev) => ({ ...prev, signingIn: true })); + setAuthState(prev => ({ ...prev, signingIn: true })); setSignInFailure(undefined); const nonce = await getNonce(); const result = await sdk.actions.signIn({ nonce }); @@ -89,7 +89,7 @@ export function SignIn() { } setSignInFailure('Unknown error'); } finally { - setAuthState((prev) => ({ ...prev, signingIn: false })); + setAuthState(prev => ({ ...prev, signingIn: false })); } }, [getNonce]); @@ -103,14 +103,14 @@ export function SignIn() { */ const handleSignOut = useCallback(async () => { try { - setAuthState((prev) => ({ ...prev, signingOut: true })); + setAuthState(prev => ({ ...prev, signingOut: true })); // Only sign out if the current session is from Farcaster provider if (session?.provider === 'farcaster') { await signOut({ redirect: false }); } setSignInResult(undefined); } finally { - setAuthState((prev) => ({ ...prev, signingOut: false })); + setAuthState(prev => ({ ...prev, signingOut: false })); } }, [session]); @@ -132,7 +132,9 @@ export function SignIn() { {/* Session Information */} {session && (
-
Session
+
+ Session +
{JSON.stringify(session, null, 2)}
@@ -142,15 +144,21 @@ export function SignIn() { {/* Error Display */} {signInFailure && !authState.signingIn && (
-
SIWF Result
-
{signInFailure}
+
+ SIWF Result +
+
+ {signInFailure} +
)} {/* Success Result Display */} {signInResult && !authState.signingIn && (
-
SIWF Result
+
+ SIWF Result +
{JSON.stringify(signInResult, null, 2)}
@@ -158,4 +166,4 @@ export function SignIn() { )} ); -} \ No newline at end of file +} diff --git a/src/hooks/useDetectClickOutside.ts b/src/hooks/useDetectClickOutside.ts index e6b1533..c1d4cf6 100644 --- a/src/hooks/useDetectClickOutside.ts +++ b/src/hooks/useDetectClickOutside.ts @@ -2,7 +2,7 @@ import { useEffect } from 'react'; export function useDetectClickOutside( ref: React.RefObject, - callback: () => void + callback: () => void, ) { useEffect(() => { function handleClickOutside(event: MouseEvent) { diff --git a/src/lib/kv.ts b/src/lib/kv.ts index f6d1de4..2963f90 100644 --- a/src/lib/kv.ts +++ b/src/lib/kv.ts @@ -1,23 +1,25 @@ -import { FrameNotificationDetails } from "@farcaster/miniapp-sdk"; -import { Redis } from "@upstash/redis"; -import { APP_NAME } from "./constants"; +import { FrameNotificationDetails } from '@farcaster/miniapp-sdk'; +import { Redis } from '@upstash/redis'; +import { APP_NAME } from './constants'; // In-memory fallback storage const localStore = new Map(); // Use Redis if KV env vars are present, otherwise use in-memory const useRedis = process.env.KV_REST_API_URL && process.env.KV_REST_API_TOKEN; -const redis = useRedis ? new Redis({ - url: process.env.KV_REST_API_URL!, - token: process.env.KV_REST_API_TOKEN!, -}) : null; +const redis = useRedis + ? new Redis({ + url: process.env.KV_REST_API_URL!, + token: process.env.KV_REST_API_TOKEN!, + }) + : null; function getUserNotificationDetailsKey(fid: number): string { return `${APP_NAME}:user:${fid}`; } export async function getUserNotificationDetails( - fid: number + fid: number, ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { @@ -28,7 +30,7 @@ export async function getUserNotificationDetails( export async function setUserNotificationDetails( fid: number, - notificationDetails: FrameNotificationDetails + notificationDetails: FrameNotificationDetails, ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { @@ -39,7 +41,7 @@ export async function setUserNotificationDetails( } export async function deleteUserNotificationDetails( - fid: number + fid: number, ): Promise { const key = getUserNotificationDetailsKey(fid); if (redis) { @@ -47,4 +49,4 @@ export async function deleteUserNotificationDetails( } else { localStore.delete(key); } -} \ No newline at end of file +} diff --git a/src/lib/notifs.ts b/src/lib/notifs.ts index 53adbef..995d54f 100644 --- a/src/lib/notifs.ts +++ b/src/lib/notifs.ts @@ -1,18 +1,18 @@ import { SendNotificationRequest, sendNotificationResponseSchema, -} from "@farcaster/miniapp-sdk"; -import { getUserNotificationDetails } from "~/lib/kv"; -import { APP_URL } from "./constants"; +} from '@farcaster/miniapp-sdk'; +import { getUserNotificationDetails } from '~/lib/kv'; +import { APP_URL } from './constants'; type SendMiniAppNotificationResult = | { - state: "error"; + state: 'error'; error: unknown; } - | { state: "no_token" } - | { state: "rate_limit" } - | { state: "success" }; + | { state: 'no_token' } + | { state: 'rate_limit' } + | { state: 'success' }; export async function sendMiniAppNotification({ fid, @@ -25,13 +25,13 @@ export async function sendMiniAppNotification({ }): Promise { const notificationDetails = await getUserNotificationDetails(fid); if (!notificationDetails) { - return { state: "no_token" }; + return { state: 'no_token' }; } const response = await fetch(notificationDetails.url, { - method: "POST", + method: 'POST', headers: { - "Content-Type": "application/json", + 'Content-Type': 'application/json', }, body: JSON.stringify({ notificationId: crypto.randomUUID(), @@ -48,17 +48,17 @@ export async function sendMiniAppNotification({ const responseBody = sendNotificationResponseSchema.safeParse(responseJson); if (responseBody.success === false) { // Malformed response - return { state: "error", error: responseBody.error.errors }; + return { state: 'error', error: responseBody.error.errors }; } if (responseBody.data.result.rateLimitedTokens.length) { // Rate limited - return { state: "rate_limit" }; + return { state: 'rate_limit' }; } - return { state: "success" }; + return { state: 'success' }; } else { // Error response - return { state: "error", error: responseJson }; + return { state: 'error', error: responseJson }; } -} \ No newline at end of file +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 2458ad4..173ed9b 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,6 +1,5 @@ -import { type ClassValue, clsx } from "clsx"; -import { twMerge } from "tailwind-merge"; -import { mnemonicToAccount } from "viem/accounts"; +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; import { APP_BUTTON_TEXT, APP_DESCRIPTION, @@ -12,8 +11,8 @@ import { APP_TAGS, APP_URL, APP_WEBHOOK_URL, -} from "./constants"; -import { APP_SPLASH_URL } from "./constants"; +} from './constants'; +import { APP_SPLASH_URL } from './constants'; interface MiniAppMetadata { version: string; @@ -45,12 +44,12 @@ export function cn(...inputs: ClassValue[]) { export function getMiniAppEmbedMetadata(ogImageUrl?: string) { return { - version: "next", + version: 'next', imageUrl: ogImageUrl ?? APP_OG_IMAGE_URL, button: { title: APP_BUTTON_TEXT, action: { - type: "launch_frame", + type: 'launch_frame', name: APP_NAME, url: APP_URL, splashImageUrl: APP_SPLASH_URL, @@ -69,37 +68,37 @@ export async function getFarcasterMetadata(): Promise { if (process.env.MINI_APP_METADATA) { try { const metadata = JSON.parse(process.env.MINI_APP_METADATA); - console.log("Using pre-signed mini app metadata from environment"); + console.log('Using pre-signed mini app metadata from environment'); return metadata; } catch (error) { console.warn( - "Failed to parse MINI_APP_METADATA from environment:", - error + 'Failed to parse MINI_APP_METADATA from environment:', + error, ); } } if (!APP_URL) { - throw new Error("NEXT_PUBLIC_URL not configured"); + throw new Error('NEXT_PUBLIC_URL not configured'); } // Get the domain from the URL (without https:// prefix) const domain = new URL(APP_URL).hostname; - console.log("Using domain for manifest:", domain); + console.log('Using domain for manifest:', domain); return { accountAssociation: { - header: "", - payload: "", - signature: "", + header: '', + payload: '', + signature: '', }, frame: { - version: "1", - name: APP_NAME ?? "Neynar Starter Kit", + version: '1', + name: APP_NAME ?? 'Neynar Starter Kit', iconUrl: APP_ICON_URL, homeUrl: APP_URL, imageUrl: APP_OG_IMAGE_URL, - buttonTitle: APP_BUTTON_TEXT ?? "Launch Mini App", + buttonTitle: APP_BUTTON_TEXT ?? 'Launch Mini App', splashImageUrl: APP_SPLASH_URL, splashBackgroundColor: APP_SPLASH_BACKGROUND_COLOR, webhookUrl: APP_WEBHOOK_URL, @@ -108,4 +107,4 @@ export async function getFarcasterMetadata(): Promise { tags: APP_TAGS, }, }; -} \ No newline at end of file +}